From a1eb7e898e24011d24be780130f5d6ea055214b5 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 6 Nov 2025 00:55:14 +0100 Subject: [PATCH 01/50] PoC: OroCommerce basic integration - added API client - added auth - added new order trigger --- .nvmrc | 2 +- bun.lock | 210 +++++++++++++++++- package.json | 2 +- .../community/orocommerce/.eslintrc.json | 33 +++ .../pieces/community/orocommerce/README.md | 7 + .../pieces/community/orocommerce/package.json | 10 + .../pieces/community/orocommerce/project.json | 65 ++++++ .../pieces/community/orocommerce/src/index.ts | 15 ++ .../orocommerce/src/lib/common/auth.ts | 36 +++ .../orocommerce/src/lib/common/client.ts | 91 ++++++++ .../orocommerce/src/lib/common/index.ts | 3 + .../src/lib/common/register-webhook.ts | 69 ++++++ .../orocommerce/src/lib/triggers/new-order.ts | 65 ++++++ .../community/orocommerce/tsconfig.json | 20 ++ .../community/orocommerce/tsconfig.lib.json | 9 + 15 files changed, 624 insertions(+), 13 deletions(-) create mode 100644 packages/pieces/community/orocommerce/.eslintrc.json create mode 100644 packages/pieces/community/orocommerce/README.md create mode 100644 packages/pieces/community/orocommerce/package.json create mode 100644 packages/pieces/community/orocommerce/project.json create mode 100644 packages/pieces/community/orocommerce/src/index.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/auth.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/client.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/index.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts create mode 100644 packages/pieces/community/orocommerce/tsconfig.json create mode 100644 packages/pieces/community/orocommerce/tsconfig.lib.json diff --git a/.nvmrc b/.nvmrc index 3462e8c26fac..209e3ef4b624 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.19.0 \ No newline at end of file +20 diff --git a/bun.lock b/bun.lock index a9eb46ec2e0f..aef77728bd04 100644 --- a/bun.lock +++ b/bun.lock @@ -290,7 +290,7 @@ "@types/marked": "4.3.2", "@types/mime-types": "2.1.1", "@types/mustache": "4.2.4", - "@types/node": "^18.16.9", + "@types/node": "20.19.9", "@types/node-cron": "3.0.11", "@types/nodemailer": "6.4.9", "@types/onfleet__node-onfleet": "1.3.7", @@ -2223,7 +2223,7 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="], "@types/node-cron": ["@types/node-cron@3.0.11", "", {}, "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg=="], @@ -3771,7 +3771,7 @@ "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], - "hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], "hpack.js": ["hpack.js@2.1.6", "", { "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", "readable-stream": "^2.0.1", "wbuf": "^1.1.0" } }, "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ=="], @@ -5115,7 +5115,7 @@ "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], "quick-temp": ["quick-temp@0.1.8", "", { "dependencies": { "mktemp": "~0.4.0", "rimraf": "^2.5.4", "underscore.string": "~3.3.4" } }, "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA=="], @@ -5863,7 +5863,7 @@ "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], - "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "unfetch": ["unfetch@4.2.0", "", {}, "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA=="], @@ -6415,6 +6415,8 @@ "@jest/core/@jest/test-result": ["@jest/test-result@30.0.5", "", { "dependencies": { "@jest/console": "30.0.5", "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" } }, "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ=="], + "@jest/core/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@jest/core/jest-config": ["jest-config@30.0.5", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.0.1", "@jest/pattern": "30.0.1", "@jest/test-sequencer": "30.0.5", "@jest/types": "30.0.5", "babel-jest": "30.0.5", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", "jest-circus": "30.0.5", "jest-docblock": "30.0.1", "jest-environment-node": "30.0.5", "jest-regex-util": "30.0.1", "jest-resolve": "30.0.5", "jest-runner": "30.0.5", "jest-util": "30.0.5", "jest-validate": "30.0.5", "micromatch": "^4.0.8", "parse-json": "^5.2.0", "pretty-format": "30.0.5", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "peerDependencies": { "@types/node": "*", "esbuild-register": ">=3.4.0", "ts-node": ">=9.0.0" }, "optionalPeers": ["@types/node", "esbuild-register", "ts-node"] }, "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA=="], "@jest/core/jest-haste-map": ["jest-haste-map@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "jest-worker": "30.0.5", "micromatch": "^4.0.8", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.3" } }, "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg=="], @@ -6469,6 +6471,8 @@ "@jest/transform/jest-haste-map": ["jest-haste-map@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "jest-worker": "30.0.5", "micromatch": "^4.0.8", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.3" } }, "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg=="], + "@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "@modern-js/node-bundle-require/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], @@ -7007,6 +7011,10 @@ "@sendgrid/client/axios": ["axios@1.13.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA=="], + "@slack/logger/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@slack/web-api/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@smithy/abort-controller/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@smithy/chunked-blob-reader/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -7135,6 +7143,10 @@ "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/amqplib/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@types/bcrypt/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/body-parser/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/bonjour/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], @@ -7149,14 +7161,24 @@ "@types/cors/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/decompress/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@types/docusign-esign/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/express-serve-static-core/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/feedparser/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/fs-extra/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/glob/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/http-proxy/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/imap/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@types/imapflow/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/jsdom/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/jsdom/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -7165,16 +7187,26 @@ "@types/keyv/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/mailparser/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/memcached/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/mysql/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/node-forge/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/nodemailer/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/oracledb/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/papaparse/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@types/pg/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/pg-pool/@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="], + "@types/qrcode/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/readable-stream/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/readdir-glob/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], @@ -7189,12 +7221,22 @@ "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/snowflake-sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/sockjs/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/tedious/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/write-file-atomic/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/ws/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@types/xml2js/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@types/xmlrpc/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/yauzl/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@7.18.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA=="], @@ -7375,8 +7417,6 @@ "camelcase-keys/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "camelcase-keys/quick-lru": ["quick-lru@4.0.1", "", {}, "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g=="], - "chai/loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "checkly/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.3", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.3", "@typescript-eslint/tsconfig-utils": "8.46.3", "@typescript-eslint/types": "8.46.3", "@typescript-eslint/visitor-keys": "8.46.3", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA=="], @@ -7671,7 +7711,7 @@ "help-me/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "hpack.js/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], @@ -7699,6 +7739,8 @@ "http-server/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "http2-wrapper/quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "i18next-parser/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "import-fresh/parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -7747,12 +7789,16 @@ "jest-each/jest-util": ["jest-util@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", "picomatch": "^4.0.2" } }, "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA=="], + "jest-environment-jsdom/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-environment-jsdom/jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], "jest-environment-node/@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], "jest-environment-node/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], + "jest-environment-node/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-environment-node/jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], "jest-haste-map/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="], @@ -7823,6 +7869,8 @@ "jest-snapshot/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-validate/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="], "jest-watcher/@jest/test-result": ["@jest/test-result@30.0.5", "", { "dependencies": { "@jest/console": "30.0.5", "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" } }, "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ=="], @@ -7989,8 +8037,6 @@ "node-gyp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "normalize-package-data/hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - "normalize-package-data/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "npm/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], @@ -8345,6 +8391,8 @@ "npm/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "npm-package-arg/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], + "npm-package-arg/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "nx/@nx/nx-darwin-arm64": ["@nx/nx-darwin-arm64@22.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kbzgsDXHwpq1+PdKC9R23dvD9XescsxF42mizrt3H615cejJe8PBF93/Cx/sdfT+n5PEmKXRDwTRBDXQbUsX9g=="], @@ -8949,6 +8997,8 @@ "@jest/core/@jest/reporters/jest-worker": ["jest-worker@30.0.5", "", { "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", "jest-util": "30.0.5", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" } }, "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ=="], + "@jest/core/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/core/jest-config/@jest/get-type": ["@jest/get-type@30.0.1", "", {}, "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw=="], "@jest/core/jest-config/@jest/test-sequencer": ["@jest/test-sequencer@30.0.5", "", { "dependencies": { "@jest/test-result": "30.0.5", "graceful-fs": "^4.2.11", "jest-haste-map": "30.0.5", "slash": "^3.0.0" } }, "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ=="], @@ -9021,10 +9071,14 @@ "@jest/reporters/jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@jest/test-result/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@jest/transform/jest-haste-map/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@jest/transform/jest-haste-map/jest-worker": ["jest-worker@30.0.5", "", { "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", "jest-util": "30.0.5", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" } }, "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ=="], + "@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -9309,18 +9363,56 @@ "@segment/analytics-node/@segment/analytics-core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@slack/logger/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@slack/web-api/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@swc-node/sourcemap-support/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "@testing-library/dom/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "@types/amqplib/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/bcrypt/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/decompress/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/docusign-esign/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/feedparser/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/imap/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/imapflow/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@types/jsdom/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "@types/mailparser/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/nodemailer/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/papaparse/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@types/pg-pool/@types/pg/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@types/pg-pool/@types/pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "@types/pg/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/qrcode/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/snowflake-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/write-file-atomic/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/xml2js/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@types/xmlrpc/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.3", "", { "dependencies": { "@typescript-eslint/types": "8.46.3", "eslint-visitor-keys": "^4.2.1" } }, "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg=="], "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -9517,6 +9609,8 @@ "expect/jest-util/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="], + "expect/jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "fastify/pino/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], @@ -9629,6 +9723,8 @@ "jest-cli/jest-validate/pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="], + "jest-config/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-config/babel-jest/@jest/transform": ["@jest/transform@30.2.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", "jest-haste-map": "30.2.0", "jest-regex-util": "30.0.1", "jest-util": "30.2.0", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" } }, "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA=="], "jest-config/babel-jest/babel-preset-jest": ["babel-preset-jest@30.2.0", "", { "dependencies": { "babel-plugin-jest-hoist": "30.2.0", "babel-preset-current-node-syntax": "^1.2.0" }, "peerDependencies": { "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ=="], @@ -9637,8 +9733,18 @@ "jest-config/jest-environment-node/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], + "jest-config/jest-environment-node/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-config/jest-environment-node/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], + "jest-config/jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "jest-each/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "jest-each/jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "jest-environment-jsdom/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-environment-jsdom/jsdom/cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], "jest-environment-jsdom/jsdom/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -9651,6 +9757,8 @@ "jest-environment-node/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "jest-environment-node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-environment-node/jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "jest-environment-node/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -9659,6 +9767,8 @@ "jest-haste-map/jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-message-util/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-mock/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "jest-mock/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], @@ -9671,6 +9781,8 @@ "jest-resolve/jest-util/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="], + "jest-resolve/jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-runner/@jest/environment/@jest/fake-timers": ["@jest/fake-timers@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.2.0", "jest-mock": "30.2.0", "jest-util": "30.2.0" } }, "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw=="], "jest-runner/@jest/environment/jest-mock": ["jest-mock@30.2.0", "", { "dependencies": { "@jest/types": "30.2.0", "@types/node": "*", "jest-util": "30.2.0" } }, "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw=="], @@ -9713,6 +9825,10 @@ "jest-snapshot/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-validate/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-watcher/@jest/test-result/@jest/console": ["@jest/console@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "jest-message-util": "30.0.5", "jest-util": "30.0.5", "slash": "^3.0.0" } }, "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA=="], "jest-worker/jest-util/@jest/types": ["@jest/types@30.2.0", "", { "dependencies": { "@jest/pattern": "30.0.1", "@jest/schemas": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", "@types/yargs": "^17.0.33", "chalk": "^4.1.2" } }, "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg=="], @@ -9857,7 +9973,7 @@ "node-gyp/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "npm/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -10141,6 +10257,10 @@ "@istanbuljs/load-nyc-config/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "@jest/console/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/console/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/core/@jest/reporters/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "@jest/core/jest-config/jest-circus/@jest/expect": ["@jest/expect@30.0.5", "", { "dependencies": { "expect": "30.0.5", "jest-snapshot": "30.0.5" } }, "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA=="], @@ -10175,14 +10295,22 @@ "@jest/environment/@jest/fake-timers/jest-message-util/pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="], + "@jest/expect/jest-snapshot/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@jest/expect/jest-snapshot/jest-util/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@jest/fake-timers/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "@jest/fake-timers/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "@jest/fake-timers/jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@jest/fake-timers/jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@jest/fake-timers/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/globals/@jest/expect/expect/@jest/expect-utils": ["@jest/expect-utils@30.0.5", "", { "dependencies": { "@jest/get-type": "30.0.1" } }, "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew=="], "@jest/globals/@jest/expect/expect/@jest/get-type": ["@jest/get-type@30.0.1", "", {}, "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw=="], @@ -10191,6 +10319,12 @@ "@jest/globals/@jest/expect/expect/jest-message-util": ["jest-message-util@30.0.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA=="], + "@jest/reporters/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/reporters/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/test-result/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/transform/jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -10273,6 +10407,8 @@ "@xhmikosr/downloader/got/cacheable-request/normalize-url": ["normalize-url@8.1.0", "", {}, "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w=="], + "@xhmikosr/downloader/got/http2-wrapper/quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "@yarnpkg/parsers/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], "babel-loader/find-cache-dir/pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="], @@ -10331,6 +10467,8 @@ "expect/jest-mock/@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "expect/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "file-loader/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "flat-cache/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -10347,8 +10485,12 @@ "jest-circus/@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + "jest-circus/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-circus/jest-runtime/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + "jest-circus/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-cli/@jest/test-result/@jest/console/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "jest-cli/@jest/test-result/@jest/console/jest-message-util": ["jest-message-util@30.0.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA=="], @@ -10369,6 +10511,8 @@ "jest-cli/jest-config/jest-environment-node/@jest/fake-timers": ["@jest/fake-timers@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", "jest-message-util": "30.0.5", "jest-mock": "30.0.5", "jest-util": "30.0.5" } }, "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw=="], + "jest-cli/jest-config/jest-environment-node/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "jest-cli/jest-config/jest-environment-node/jest-mock": ["jest-mock@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "jest-util": "30.0.5" } }, "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ=="], "jest-cli/jest-config/jest-resolve/jest-haste-map": ["jest-haste-map@30.0.5", "", { "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "jest-worker": "30.0.5", "micromatch": "^4.0.8", "walker": "^1.0.8" }, "optionalDependencies": { "fsevents": "^2.3.3" } }, "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg=="], @@ -10397,6 +10541,8 @@ "jest-cli/jest-validate/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "jest-config/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-config/babel-jest/babel-preset-jest/babel-plugin-jest-hoist": ["babel-plugin-jest-hoist@30.2.0", "", { "dependencies": { "@types/babel__core": "^7.20.5" } }, "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA=="], "jest-config/jest-environment-node/@jest/environment/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], @@ -10405,20 +10551,44 @@ "jest-config/jest-environment-node/@jest/fake-timers/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "jest-config/jest-environment-node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-config/jest-environment-node/jest-mock/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "jest-config/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-each/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-each/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-environment-jsdom/jsdom/parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "jest-environment-node/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "jest-haste-map/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-haste-map/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-message-util/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-mock/@jest/types/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + "jest-mock/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-mock/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jest-resolve/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-runner/@jest/environment/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + "jest-runner/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-runner/jest-environment-node/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], "jest-runner/jest-environment-node/@jest/fake-timers/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "jest-runner/jest-environment-node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-runner/jest-environment-node/jest-mock/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "jest-runner/jest-runtime/@jest/fake-timers/@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], @@ -10427,6 +10597,8 @@ "jest-runner/jest-runtime/jest-snapshot/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "jest-runner/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-runtime/jest-haste-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "jest-runtime/jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -10439,8 +10611,12 @@ "jest-snapshot/expect/jest-mock/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "jest-validate/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-watcher/@jest/test-result/@jest/console/jest-message-util": ["jest-message-util@30.0.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA=="], + "jest-worker/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "json-server/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "jsonlint-mod/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -10633,6 +10809,8 @@ "@jest/core/jest-runner/jest-environment-node/@jest/fake-timers/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], + "@jest/core/jest-runner/jest-environment-node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/core/jest-runner/jest-environment-node/jest-mock/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "@jest/environment-jsdom-abstract/@jest/fake-timers/jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -10643,6 +10821,10 @@ "@jest/environment/@jest/fake-timers/jest-message-util/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "@jest/expect/jest-snapshot/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@jest/expect/jest-snapshot/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@jest/fake-timers/jest-message-util/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], "@jest/globals/@jest/expect/expect/jest-matcher-utils/jest-diff": ["jest-diff@30.0.5", "", { "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.0.1", "chalk": "^4.1.2", "pretty-format": "30.0.5" } }, "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A=="], @@ -10673,8 +10855,12 @@ "css-minimizer-webpack-plugin/jest-worker/jest-util/@jest/types/@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + "css-minimizer-webpack-plugin/jest-worker/jest-util/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "eslint-plugin-testing-library/@typescript-eslint/utils/@typescript-eslint/typescript-estree/globby/array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "expect/jest-mock/@jest/types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "flat-cache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "jest-cli/@jest/test-result/@jest/console/jest-message-util/pretty-format": ["pretty-format@30.0.5", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw=="], @@ -10693,6 +10879,8 @@ "jest-cli/jest-config/jest-environment-node/@jest/fake-timers/jest-message-util": ["jest-message-util@30.0.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" } }, "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA=="], + "jest-cli/jest-config/jest-environment-node/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "jest-cli/jest-config/jest-environment-node/jest-mock/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], "jest-cli/jest-config/jest-resolve/jest-haste-map/@types/node": ["@types/node@20.5.1", "", {}, "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg=="], diff --git a/package.json b/package.json index 5a4e15f7c549..e56edcf3dad3 100644 --- a/package.json +++ b/package.json @@ -316,7 +316,7 @@ "@types/marked": "4.3.2", "@types/mime-types": "2.1.1", "@types/mustache": "4.2.4", - "@types/node": "^18.16.9", + "@types/node": "20.19.9", "@types/node-cron": "3.0.11", "@types/nodemailer": "6.4.9", "@types/onfleet__node-onfleet": "1.3.7", diff --git a/packages/pieces/community/orocommerce/.eslintrc.json b/packages/pieces/community/orocommerce/.eslintrc.json new file mode 100644 index 000000000000..4a4e695c547e --- /dev/null +++ b/packages/pieces/community/orocommerce/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../../../.eslintrc.base.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md new file mode 100644 index 000000000000..51b0d3a5f59e --- /dev/null +++ b/packages/pieces/community/orocommerce/README.md @@ -0,0 +1,7 @@ +# pieces-orocommerce + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build pieces-orocommerce` to build the library. diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json new file mode 100644 index 000000000000..eb61935e4b4c --- /dev/null +++ b/packages/pieces/community/orocommerce/package.json @@ -0,0 +1,10 @@ +{ + "name": "@activepieces/piece-orocommerce", + "version": "0.0.1", + "type": "commonjs", + "main": "./src/index.js", + "types": "./src/index.d.ts", + "dependencies": { + "tslib": "^2.3.0" + } +} diff --git a/packages/pieces/community/orocommerce/project.json b/packages/pieces/community/orocommerce/project.json new file mode 100644 index 000000000000..846293c7b644 --- /dev/null +++ b/packages/pieces/community/orocommerce/project.json @@ -0,0 +1,65 @@ +{ + "name": "pieces-orocommerce", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/orocommerce/src", + "projectType": "library", + "release": { + "version": { + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ], + "currentVersionResolver": "git-tag", + "fallbackCurrentVersionResolver": "disk" + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/orocommerce", + "tsConfig": "packages/pieces/community/orocommerce/tsconfig.lib.json", + "packageJson": "packages/pieces/community/orocommerce/package.json", + "main": "packages/pieces/community/orocommerce/src/index.ts", + "assets": [ + "packages/pieces/community/orocommerce/*.md", + { + "input": "packages/pieces/community/orocommerce/src/i18n", + "output": "./src/i18n", + "glob": "**/!(i18n.json)" + } + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true + }, + "dependsOn": [ + "prebuild", + "^build" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "prebuild": { + "dependsOn": [ + "^build" + ], + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/orocommerce", + "command": "bun install --no-save" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} \ No newline at end of file diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts new file mode 100644 index 000000000000..5bff0ec81489 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -0,0 +1,15 @@ +import { createPiece } from '@activepieces/pieces-framework'; +import { orocommerceAuth } from './lib/common/auth'; +import { newOrder } from './lib/triggers/new-order'; + +export const orocommerce = createPiece({ + displayName: 'OroCommerce', + auth: orocommerceAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://oroinc.com/wp-content/themes/oroinc/images/redesign/ORO.svg', + authors: [], + actions: [], + triggers: [ + newOrder + ] +}); diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts new file mode 100644 index 000000000000..342b65dc6935 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -0,0 +1,36 @@ +import { PieceAuth, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { oroCommerceApiCall } from './client'; + +export const orocommerceAuth = PieceAuth.CustomAuth({ + props: { + serverUrl: Property.ShortText({ + displayName: 'Server URL', + required: true, + }), + clientId: Property.ShortText({ + displayName: 'Client ID', + required: true, + }), + clientSecret: PieceAuth.SecretText({ + displayName: 'Client Secret', + required: true, + }), + }, + validate: async ({ auth }) => { + try { + await oroCommerceApiCall({ + method: HttpMethod.GET, + resourceUri: '/admin/api/regions/US-CA', + auth, + }); + return { valid: true }; + } catch { + return { + valid: false, + error: 'Check Server URL, Client ID and Client Secret', + }; + } + }, + required: true, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts new file mode 100644 index 000000000000..09a62e14ace4 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -0,0 +1,91 @@ +import { + httpClient, + HttpMethod, + HttpRequest, + HttpMessageBody, + QueryParams, HttpResponse +} from '@activepieces/pieces-common'; + +export type OroCommerceAuthProps = { + serverUrl: string; + clientId: string; + clientSecret: string; +}; + +let cachedToken: string | null = null; +let tokenExpiresAt = 0; + +async function getAccessToken(auth: OroCommerceAuthProps): Promise { + if (cachedToken && Date.now() < tokenExpiresAt) { + return cachedToken; + } + + const response = await httpClient.sendRequest<{ + token_type: string; + access_token: string; + expires_in: number; + }>({ + method: HttpMethod.POST, + url: auth.serverUrl + '/oauth2-token', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: { + grant_type: 'client_credentials', + client_id: auth.clientId, + client_secret: auth.clientSecret, + }, + }); + + cachedToken = response.body.access_token; + tokenExpiresAt = Date.now() + response.body.expires_in * 1000 - 30 * 1000; + + return cachedToken!; +} + +export type OroCommerceApiCallParams = { + method: HttpMethod; + resourceUri: string; + query?: Record; + body?: any; + auth: OroCommerceAuthProps; +}; + +export async function oroCommerceApiCall({ + method, + resourceUri, + query, + body, + auth, +}: OroCommerceApiCallParams): Promise> { + const token = await getAccessToken(auth); + + const queryParams: QueryParams = {}; + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value !== null && value !== undefined) { + queryParams[key] = String(value); + } + } + } + + const request: HttpRequest = { + method, + url: `${auth.serverUrl}${resourceUri}`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/vnd.api+json', + }, + queryParams, + body, + }; + + try { + return await httpClient.sendRequest(request); + } catch (error: any) { + const statusCode = error.response?.status; + const errorMessage = error.response?.data?.message || error.message || 'Unknown error'; + + throw new Error(`OroCommerce API Error (${statusCode || 'Unknown'}): ${errorMessage}`); + } +} diff --git a/packages/pieces/community/orocommerce/src/lib/common/index.ts b/packages/pieces/community/orocommerce/src/lib/common/index.ts new file mode 100644 index 000000000000..9f3087bf9e32 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -0,0 +1,3 @@ +export * from './auth'; +export * from './client'; +export * from './register-webhook'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts new file mode 100644 index 000000000000..8bed80c32c6b --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts @@ -0,0 +1,69 @@ +import { + createTrigger, + Trigger, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { orocommerceAuth } from './auth'; +import { oroCommerceApiCall } from './client'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const createOroCommerceWebhookTrigger = ({ + name, + description, + displayName, + topic, + event, + sampleData +}: { + name: string; + description: string; + displayName: string; + topic: string; + event: string; + sampleData: Record; +}): Trigger => + createTrigger({ + auth: orocommerceAuth, + name, + description, + displayName, + props: {}, + sampleData: sampleData, + type: TriggerStrategy.WEBHOOK, + + async onEnable(context) { + const response = await oroCommerceApiCall({ + method: HttpMethod.POST, + resourceUri: '/admin/api/remotenotifications', + auth: context.auth, + body: { + data: { + type: 'remotenotifications', + attributes: { + channel: topic, + event: event, + enabled: true, + notificationUrl: context.webhookUrl + } + } + } + }); + + await context.store?.put(`orocommerce_webhook_id`, response.body.data.id); + console.log('webhook created', response.body.data.id); + }, + + async onDisable(context) { + const webhookId = await context.store.get(`orocommerce_webhook_id`); + await oroCommerceApiCall({ + method: HttpMethod.DELETE, + resourceUri: '/admin/api/remotenotifications/' + webhookId, + auth: context.auth + }); + await context.store?.put(`orocommerce_webhook_id`, null); + }, + async run(context) { + console.debug('trigger running', context); + return [context.payload.body]; + }, + }); diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts b/packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts new file mode 100644 index 000000000000..346acf2f4baa --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts @@ -0,0 +1,65 @@ +import { createOroCommerceWebhookTrigger } from '../common/register-webhook'; + +export const newOrder = createOroCommerceWebhookTrigger({ + name: 'new_order', + description: 'Triggered when a new order is created', + topic: 'order', + event: 'create', + displayName: 'New Order', + sampleData: { + "event": "create", + "channel": "order", + "data": { + "id": 26, + "identifier": "26", + "poNumber": "PONUM111", + "customerNotes": "Some notes", + "shipUntil": { + "date": "2025-11-28 00:00:00.000000", + "timezone_type": 3, + "timezone": "UTC" + }, + "currency": "USD", + "subtotalWithDiscountsCurrency": "USD", + "subtotalWithDiscountsValue": 788.95, + "subtotalWithDiscounts": 788.95, + "subtotalCurrency": "USD", + "subtotalValue": 808.95, + "baseSubtotalValue": 808.95, + "totalCurrency": "USD", + "totalValue": 798.95, + "baseTotalValue": 798.95, + "shippingMethod": "flat_rate_6", + "shippingMethodType": "primary", + "estimatedShippingCostAmount": 10, + "overriddenShippingCostAmount": null, + "sourceEntityClass": "Oro\\Bundle\\ShoppingListBundle\\Entity\\ShoppingList", + "sourceEntityId": 1, + "sourceEntityIdentifier": "Care Team Reorder", + "totalDiscountsAmount": null, + "external": false, + "createdAt": { + "date": "2025-11-05 23:15:25.000000", + "timezone_type": 3, + "timezone": "UTC" + }, + "updatedAt": { + "date": "2025-11-05 23:15:25.000000", + "timezone_type": 3, + "timezone": "UTC" + }, + "ac_last_contact_date": null, + "ac_last_contact_date_in": null, + "ac_last_contact_date_out": null, + "ac_contact_count": null, + "ac_contact_count_in": null, + "ac_contact_count_out": null, + "serialized_data": { + "internal_status": "order_internal_status.open", + "shippingStatus": "order_shipping_status.not_shipped" + }, + "disablePromotions": false + }, + "timestamp": 1762384555 + } +}); diff --git a/packages/pieces/community/orocommerce/tsconfig.json b/packages/pieces/community/orocommerce/tsconfig.json new file mode 100644 index 000000000000..eff240ac143e --- /dev/null +++ b/packages/pieces/community/orocommerce/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/orocommerce/tsconfig.lib.json b/packages/pieces/community/orocommerce/tsconfig.lib.json new file mode 100644 index 000000000000..5995d959bf0c --- /dev/null +++ b/packages/pieces/community/orocommerce/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} From cc9206a652ce8a1537cf84ea2540e0a188d8cef4 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 4 Feb 2026 13:50:07 +0100 Subject: [PATCH 02/50] refactor: update orocommerce initial integration code --- .../pieces/community/orocommerce/package.json | 8 +- .../pieces/community/orocommerce/src/index.ts | 12 ++- .../orocommerce/src/lib/common/auth.ts | 41 ++++++++- .../orocommerce/src/lib/common/client.ts | 91 ++++++++----------- .../src/lib/common/register-webhook.ts | 52 +++++++---- .../orocommerce/src/lib/common/types.ts | 25 +++++ 6 files changed, 141 insertions(+), 88 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/lib/common/types.ts diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index eb61935e4b4c..d21383c44e8d 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -1,10 +1,4 @@ { "name": "@activepieces/piece-orocommerce", - "version": "0.0.1", - "type": "commonjs", - "main": "./src/index.js", - "types": "./src/index.d.ts", - "dependencies": { - "tslib": "^2.3.0" - } + "version": "0.0.1" } diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index 5bff0ec81489..85cb355a88cd 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -1,15 +1,19 @@ import { createPiece } from '@activepieces/pieces-framework'; -import { orocommerceAuth } from './lib/common/auth'; +import { PieceCategory } from '@activepieces/shared'; +import { orocommerceAuth } from './lib/common'; import { newOrder } from './lib/triggers/new-order'; export const orocommerce = createPiece({ displayName: 'OroCommerce', auth: orocommerceAuth, minimumSupportedRelease: '0.36.1', - logoUrl: 'https://oroinc.com/wp-content/themes/oroinc/images/redesign/ORO.svg', - authors: [], + logoUrl: + 'https://oroinc.com/wp-content/themes/oroinc/images/redesign/ORO.svg', + categories: [PieceCategory.COMMERCE], + description: 'B2B digital commerce solution', + authors: ['Oro Inc.'], actions: [], triggers: [ newOrder - ] + ], }); diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 342b65dc6935..7834da5a5182 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -1,36 +1,69 @@ import { PieceAuth, Property } from '@activepieces/pieces-framework'; import { HttpMethod } from '@activepieces/pieces-common'; import { oroCommerceApiCall } from './client'; +import { AppConnectionType } from '@activepieces/shared'; export const orocommerceAuth = PieceAuth.CustomAuth({ + description: ` +Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. + +**Steps to obtain credentials:** +1. Log in to your OroCommerce admin panel. +2. Navigate to **System** > **User Management** > **OAuth Applications**. +3. Click **Create OAuth Application** and configure: + - **Application Name**: Enter a descriptive name (e.g., "Activepieces Integration") + - **Grants**: Select **Client Credentials** + - **Redirect URIs**: Not required for Client Credentials flow +4. Save the application and copy the **Client ID** and **Client Secret**. +5. Note your **Server URL** (e.g., \`https://your-store.com\`) and **Admin Prefix** (usually \`admin\`). + `, props: { serverUrl: Property.ShortText({ displayName: 'Server URL', + description: + 'The base URL of your OroCommerce instance (e.g., https://your-store.com).', + required: true, + }), + adminPrefix: Property.ShortText({ + displayName: 'Admin Prefix', + description: 'The admin panel URL prefix (default is "admin").', required: true, + defaultValue: 'admin', }), clientId: Property.ShortText({ displayName: 'Client ID', + description: + 'The OAuth Client ID from your OroCommerce OAuth application.', required: true, }), clientSecret: PieceAuth.SecretText({ displayName: 'Client Secret', + description: + 'The OAuth Client Secret from your OroCommerce OAuth application.', required: true, }), }, + validate: async ({ auth }) => { try { await oroCommerceApiCall({ method: HttpMethod.GET, - resourceUri: '/admin/api/regions/US-CA', - auth, + resourceUri: '/regions/US-CA', + auth: { + type: AppConnectionType.CUSTOM_AUTH, + props: auth, + }, }); return { valid: true }; - } catch { + } catch (e: any) { return { valid: false, - error: 'Check Server URL, Client ID and Client Secret', + error: + e?.message || + 'Invalid credentials. Please verify your Server URL, Admin Prefix, Client ID, and Client Secret.', }; } }, + required: true, }); diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index 09a62e14ace4..af4b5a2081f1 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -1,91 +1,72 @@ import { httpClient, HttpMethod, - HttpRequest, HttpMessageBody, - QueryParams, HttpResponse + HttpResponse, + AuthenticationType, } from '@activepieces/pieces-common'; -export type OroCommerceAuthProps = { - serverUrl: string; - clientId: string; - clientSecret: string; -}; +import { + type OroCommerceAuth, + type OroCommerceAuthResponseType, + type OroCommerceApiCallParams, +} from './types'; let cachedToken: string | null = null; let tokenExpiresAt = 0; -async function getAccessToken(auth: OroCommerceAuthProps): Promise { +async function getAccessToken(auth: OroCommerceAuth): Promise { if (cachedToken && Date.now() < tokenExpiresAt) { return cachedToken; } - const response = await httpClient.sendRequest<{ - token_type: string; - access_token: string; - expires_in: number; - }>({ + const response = await httpClient.sendRequest({ method: HttpMethod.POST, - url: auth.serverUrl + '/oauth2-token', + url: auth.props.serverUrl + '/oauth2-token', headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', }, body: { grant_type: 'client_credentials', - client_id: auth.clientId, - client_secret: auth.clientSecret, + client_id: auth.props.clientId, + client_secret: auth.props.clientSecret, }, }); cachedToken = response.body.access_token; tokenExpiresAt = Date.now() + response.body.expires_in * 1000 - 30 * 1000; - return cachedToken!; + return cachedToken; } -export type OroCommerceApiCallParams = { - method: HttpMethod; - resourceUri: string; - query?: Record; - body?: any; - auth: OroCommerceAuthProps; -}; - -export async function oroCommerceApiCall({ +export async function oroCommerceApiCall({ method, resourceUri, - query, - body, auth, -}: OroCommerceApiCallParams): Promise> { - const token = await getAccessToken(auth); - - const queryParams: QueryParams = {}; - if (query) { - for (const [key, value] of Object.entries(query)) { - if (value !== null && value !== undefined) { - queryParams[key] = String(value); - } - } - } - - const request: HttpRequest = { - method, - url: `${auth.serverUrl}${resourceUri}`, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/vnd.api+json', - }, - queryParams, - body, - }; - + queryParams, + body, +}: OroCommerceApiCallParams): Promise> { try { - return await httpClient.sendRequest(request); + return await httpClient.sendRequest({ + method, + url: `${auth.props.serverUrl}/${auth.props.adminPrefix}/api${resourceUri}`, + headers: { + 'Content-Type': 'application/vnd.api+json', + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: await getAccessToken(auth), + }, + queryParams, + body, + }); } catch (error: any) { const statusCode = error.response?.status; - const errorMessage = error.response?.data?.message || error.message || 'Unknown error'; + const errorMessage = + error.response?.data?.message || error.message || 'Unknown error'; - throw new Error(`OroCommerce API Error (${statusCode || 'Unknown'}): ${errorMessage}`); + throw new Error( + `OroCommerce API Error (${statusCode || 'Unknown'}): ${errorMessage}` + ); } } diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts index 8bed80c32c6b..98c698f828f5 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts @@ -13,7 +13,7 @@ export const createOroCommerceWebhookTrigger = ({ displayName, topic, event, - sampleData + sampleData, }: { name: string; description: string; @@ -28,42 +28,58 @@ export const createOroCommerceWebhookTrigger = ({ description, displayName, props: {}, - sampleData: sampleData, + sampleData, type: TriggerStrategy.WEBHOOK, async onEnable(context) { const response = await oroCommerceApiCall({ method: HttpMethod.POST, - resourceUri: '/admin/api/remotenotifications', + resourceUri: '/remotenotifications', auth: context.auth, body: { data: { type: 'remotenotifications', attributes: { channel: topic, - event: event, + event, enabled: true, - notificationUrl: context.webhookUrl - } - } - } + notificationUrl: context.webhookUrl, + }, + }, + }, }); - await context.store?.put(`orocommerce_webhook_id`, response.body.data.id); - console.log('webhook created', response.body.data.id); + await context.store.put( + `_orocommerce_${name}_trigger`, + { + webhookId: response.body.data.id, + topic, + event, + } + ); }, async onDisable(context) { - const webhookId = await context.store.get(`orocommerce_webhook_id`); - await oroCommerceApiCall({ - method: HttpMethod.DELETE, - resourceUri: '/admin/api/remotenotifications/' + webhookId, - auth: context.auth - }); - await context.store?.put(`orocommerce_webhook_id`, null); + const webhookInfo = await context.store.get( + `_orocommerce_${name}_trigger` + ); + + if (webhookInfo !== null && webhookInfo !== undefined) { + await oroCommerceApiCall({ + method: HttpMethod.DELETE, + resourceUri: `/remotenotifications/${webhookInfo.webhookId}`, + auth: context.auth, + }); + } }, + async run(context) { - console.debug('trigger running', context); return [context.payload.body]; }, }); + +interface WebhookInformation { + webhookId: string; + topic: string; + event: string; +} diff --git a/packages/pieces/community/orocommerce/src/lib/common/types.ts b/packages/pieces/community/orocommerce/src/lib/common/types.ts new file mode 100644 index 000000000000..2585a2c31af4 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -0,0 +1,25 @@ +import { + HttpMessageBody, + HttpMethod, + QueryParams, +} from '@activepieces/pieces-common'; + +import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework'; + +import { orocommerceAuth } from './auth'; + +export type OroCommerceAuth = AppConnectionValueForAuthProperty; + +export type OroCommerceAuthResponseType = { + token_type: string; + access_token: string; + expires_in: number; +}; + +export type OroCommerceApiCallParams = { + method: HttpMethod; + resourceUri: string; + auth: OroCommerceAuth; + queryParams?: QueryParams; + body?: HttpMessageBody; +}; From edf3a6d7b0152be7c7d93f4eb2d54e8fa4d5eed6 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Fri, 20 Feb 2026 19:40:25 +0100 Subject: [PATCH 03/50] feat(orocommerce): added more triggers, added create order and create invoice actions --- .../pieces/community/orocommerce/package.json | 2 +- .../pieces/community/orocommerce/src/index.ts | 17 +- .../src/lib/actions/create-invoice.ts | 380 +++++++++++ .../src/lib/actions/create-order.ts | 531 +++++++++++++++ .../orocommerce/src/lib/actions/index.ts | 3 + .../orocommerce/src/lib/common/auth.ts | 6 +- .../orocommerce/src/lib/common/client.ts | 14 +- .../orocommerce/src/lib/common/index.ts | 1 + .../orocommerce/src/lib/common/props.ts | 546 +++++++++++++++ .../src/lib/common/register-webhook.ts | 18 +- .../orocommerce/src/lib/common/types.ts | 10 +- .../orocommerce/src/lib/examples/invoice.ts | 296 +++++++++ .../orocommerce/src/lib/examples/order.ts | 628 ++++++++++++++++++ .../orocommerce/src/lib/triggers/invoice.ts | 29 + .../orocommerce/src/lib/triggers/new-order.ts | 65 -- .../orocommerce/src/lib/triggers/order.ts | 29 + 16 files changed, 2480 insertions(+), 95 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/create-order.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/index.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/props.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/examples/invoice.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/examples/order.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts delete mode 100644 packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/triggers/order.ts diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index d21383c44e8d..0f67e61ad8b0 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-orocommerce", - "version": "0.0.1" + "version": "0.0.2" } diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index 85cb355a88cd..cb424e61eda5 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -1,19 +1,26 @@ import { createPiece } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/shared'; -import { orocommerceAuth } from './lib/common'; -import { newOrder } from './lib/triggers/new-order'; +import { oroAuth } from './lib/common'; +import { newOrder, removedOrder, updatedOrder } from './lib/triggers/order'; +import { newInvoice, removedInvoice, updatedInvoice } from './lib/triggers/invoice'; +import { createInvoiceAction, createOrderAction } from './lib/actions'; export const orocommerce = createPiece({ displayName: 'OroCommerce', - auth: orocommerceAuth, + auth: oroAuth, minimumSupportedRelease: '0.36.1', logoUrl: 'https://oroinc.com/wp-content/themes/oroinc/images/redesign/ORO.svg', categories: [PieceCategory.COMMERCE], description: 'B2B digital commerce solution', authors: ['Oro Inc.'], - actions: [], + actions: [createInvoiceAction, createOrderAction], triggers: [ - newOrder + newOrder, + removedOrder, + updatedOrder, + newInvoice, + removedInvoice, + updatedInvoice, ], }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts new file mode 100644 index 000000000000..7060cdb58a87 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -0,0 +1,380 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerDropdown, + customerUserDropdown, + invoiceInternalStatusDropdown, + organizationDropdown, + ownerDropdown, + websiteDropdown +} from '../common'; +import { OroAuth } from '../common/types'; + +export const createInvoiceAction = createAction({ + auth: oroAuth, + name: 'create_invoice', + displayName: 'Create Invoice', + description: 'Creates a new invoice record in OroCommerce.', + props: { + invoiceDate: Property.ShortText({ + displayName: 'Invoice Date', + description: 'Invoice date in YYYY-MM-DD format.', + required: true, + }), + currency: Property.ShortText({ + displayName: 'Currency', + description: 'ISO-4217 3-letter currency code (e.g. USD, EUR).', + required: true, + defaultValue: 'USD', + }), + customerName: Property.ShortText({ + displayName: 'Customer Name', + description: + 'Name of the company being billed. Stored as a plain-text label on the invoice.', + required: true, + }), + customer: customerDropdown, + customerId: Property.ShortText({ + displayName: 'Customer: Raw ID', + description: + 'Overrides the Customer dropdown. Paste an ID from a previous step.', + required: false, + }), + customerUser: customerUserDropdown(false), + customerUserId: Property.ShortText({ + displayName: 'Customer User: Raw ID', + description: 'Overrides the Customer User dropdown.', + required: false, + }), + refCustomerId: Property.ShortText({ + displayName: 'External Customer ID', + description: + 'An optional ID reference to a customer. Can be used for storing an arbitrary external ID.', + required: false, + }), + refCustomerUserId: Property.ShortText({ + displayName: 'External Customer User ID', + description: + 'An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.', + required: false, + }), + totalAmount: Property.Number({ + displayName: 'Total Amount', + description: + 'Total invoice amount. Should equal the sum of all line item row totals.', + required: true, + }), + invoiceNumber: Property.ShortText({ + displayName: 'Invoice Number', + description: + 'Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.', + required: false, + }), + title: Property.ShortText({ + displayName: 'Title', + description: 'Alternative invoice title that reflects its nature.', + required: false, + }), + description: Property.LongText({ + displayName: 'Description', + description: 'Internal description of the invoice.', + required: false, + }), + memo: Property.ShortText({ + displayName: 'Memo', + description: 'Short memo visible on the invoice (e.g. "Thank you!").', + required: false, + }), + billTo: Property.LongText({ + displayName: 'Bill To', + description: + 'Billing address HTML string (e.g. 123 Main St, City, Country).', + required: false, + }), + shipTo: Property.LongText({ + displayName: 'Ship To', + description: 'Shipping address HTML string.', + required: false, + }), + shippingMethod: Property.ShortText({ + displayName: 'Shipping Method', + description: + 'Shipping method label (e.g. "International Shipping (Tracking #: 123)").', + required: false, + }), + sellerInfo: Property.LongText({ + displayName: 'Seller Info', + description: 'Seller contact / address HTML string.', + required: false, + }), + externalPaymentUrl: Property.ShortText({ + displayName: 'External Payment URL', + description: 'URL for the external payment page.', + required: false, + }), + + organization: organizationDropdown, + organizationId: Property.ShortText({ + displayName: 'Organization: Raw ID', + description: 'Overrides the Organization dropdown.', + required: false, + }), + owner: ownerDropdown, + ownerId: Property.ShortText({ + displayName: 'Owner: Raw ID', + description: 'Overrides the Owner dropdown.', + required: false, + }), + website: websiteDropdown, + websiteId: Property.ShortText({ + displayName: 'Website: Raw ID', + description: 'Overrides the Website dropdown.', + required: false, + }), + internalStatus: invoiceInternalStatusDropdown, + internalStatusId: Property.ShortText({ + displayName: 'Internal Status: Raw ID', + description: + 'Overrides the Internal Status dropdown (e.g. draft, open, paid).', + required: false, + }), + + // -- Line Items ------------------------------------------------------------ + // DynamicProperties lets us load product units once as StaticDropdown. + // Products are entered as SKU/ID text (framework does not support Dropdown inside Array). + lineItems: Property.DynamicProperties({ + auth: oroAuth, + displayName: 'Line Items', + description: + 'Invoice line items. Each item is sent via JSON:API included.', + required: true, + refreshers: [], + props: async ({ auth }) => { + type JsonApiCollection = { + data: { id: string; attributes: Record }[]; + }; + const unitOptions: { label: string; value: string }[] = []; + + if (auth) { + try { + const unitsResp = await oroApiCall({ + method: HttpMethod.GET, + resourceUri: '/productunits', + auth: auth as OroAuth, + queryParams: { 'page[size]': '100' }, + }); + for (const item of (unitsResp.body as JsonApiCollection).data ?? + []) { + unitOptions.push({ + label: String(item.attributes['label'] || item.id), + value: item.id, + }); + } + } catch { + /* leave empty */ + } + } + + return { + lineItems: Property.Array({ + displayName: 'Line Items', + required: true, + properties: { + lineNumber: Property.ShortText({ + displayName: 'Line Number', + description: 'Display line number (e.g. 1.1, 1.2).', + required: false, + }), + description: Property.ShortText({ + displayName: 'Description', + description: 'Line item description (HTML allowed).', + required: true, + }), + productSku: Property.ShortText({ + displayName: 'Product SKU or ID', + description: + 'Enter the product SKU or numeric ID. Use the top-level "Product (Search)" field to look up products.', + required: false, + }), + productUnit: Property.StaticDropdown({ + displayName: 'Product Unit', + description: 'Unit of measurement (e.g. piece, kg, set).', + required: false, + options: { disabled: false, options: unitOptions }, + }), + productUnitId: Property.ShortText({ + displayName: 'Product Unit: Raw ID', + description: + 'Overrides the Product Unit dropdown (e.g. "piece", "kg").', + required: false, + }), + quantity: Property.Number({ + displayName: 'Quantity', + description: 'Quantity of the item.', + required: true, + }), + unitPrice: Property.Number({ + displayName: 'Unit Price', + description: 'Price per unit.', + required: true, + }), + rowTotal: Property.Number({ + displayName: 'Row Total', + description: + 'Total amount for this line (quantity × unit price).', + required: true, + }), + note: Property.ShortText({ + displayName: 'Note', + description: 'Additional note for this line item.', + required: false, + }), + }, + }), + }; + }, + }), + }, + + async run(context) { + const p = context.propsValue; + + // raw ID takes priority over dropdown selection when both are present + const resolve = ( + rawId: string | null | undefined, + dropdownVal: unknown + ): string | null => { + const raw = rawId?.trim(); + if (raw) return raw; + if (dropdownVal != null && String(dropdownVal).trim()) + return String(dropdownVal).trim(); + return null; + }; + + // -- Build included line items with string IDs -------------------------- + // DynamicProperties wraps the array in an object keyed by "lineItems" + const dynamicValue = (p.lineItems ?? {}) as Record; + const rawItems = (dynamicValue['lineItems'] ?? []) as Array< + Record + >; + + const included = rawItems.map((item, index) => { + const productSku = (item['productSku'] as string | undefined)?.trim(); + const productUnitId = + (item['productUnitId'] as string | undefined)?.trim() || + (item['productUnit'] as string | undefined); + + const attrs: Record = { + position: index + 1, + lineNumber: item['lineNumber'] || String(index + 1), + description: item['description'], + quantity: Number(item['quantity']), + unitPrice: Number(item['unitPrice']), + rowTotal: Number(item['rowTotal']), + }; + if (item['note']) attrs['note'] = item['note']; + + const liRels: Record = {}; + if (productSku) { + liRels['product'] = { data: { type: 'products', id: productSku } }; + } + if (productUnitId) { + liRels['productUnit'] = { + data: { type: 'productunits', id: productUnitId }, + }; + } + + return { + type: 'invoicelineitems', + id: `li_${index + 1}`, + attributes: attrs, + ...(Object.keys(liRels).length > 0 ? { relationships: liRels } : {}), + }; + }); + + const lineItemsRelData = included.map((li) => ({ + type: 'invoicelineitems', + id: li.id, + })); + + // -- Build attributes --------------------------------------------------- + const attributes: Record = { + invoiceDate: p.invoiceDate, + currency: p.currency, + customerName: p.customerName, + totalAmount: p.totalAmount, + }; + if (p.invoiceNumber) attributes['invoiceNumber'] = p.invoiceNumber; + if (p.title) attributes['title'] = p.title; + if (p.description) attributes['description'] = p.description; + if (p.memo) attributes['memo'] = p.memo; + if (p.billTo) attributes['billTo'] = p.billTo; + if (p.shipTo) attributes['shipTo'] = p.shipTo; + if (p.shippingMethod) attributes['shippingMethod'] = p.shippingMethod; + if (p.sellerInfo) attributes['sellerInfo'] = p.sellerInfo; + if (p.externalPaymentUrl) + attributes['externalPaymentUrl'] = p.externalPaymentUrl; + + // -- Build relationships ------------------------------------------------ + const relationships: Record = { + lineItems: { data: lineItemsRelData }, + }; + + const customerId = resolve(p.customerId, p.customer); + if (customerId) { + relationships['customer'] = { + data: { type: 'customers', id: customerId }, + }; + } + + const customerUserId = resolve(p.customerUserId, p.customerUser); + if (customerUserId) { + relationships['customer_user'] = { + data: { type: 'customerusers', id: customerUserId }, + }; + } + + const organizationId = resolve(p.organizationId, p.organization); + if (organizationId) { + relationships['organization'] = { + data: { type: 'organizations', id: organizationId }, + }; + } + + const ownerId = resolve(p.ownerId, p.owner); + if (ownerId) { + relationships['owner'] = { data: { type: 'users', id: ownerId } }; + } + + const websiteId = resolve(p.websiteId, p.website); + if (websiteId) { + relationships['website'] = { data: { type: 'websites', id: websiteId } }; + } + + const internalStatusId = resolve(p.internalStatusId, p.internalStatus); + if (internalStatusId) { + relationships['internal_status'] = { + data: { type: 'invoiceinternalstatuses', id: internalStatusId }, + }; + } + + // -- POST /invoices ----------------------------------------------------- + const response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: '/invoices', + auth: context.auth, + body: { + data: { + type: 'invoices', + attributes, + relationships, + }, + included, + }, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts new file mode 100644 index 000000000000..1f9f8f8a82a1 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -0,0 +1,531 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerRequiredDropdown, + customerUserDropdown, + organizationDropdown, + ownerDropdown, + websiteDropdown, + orderInternalStatusDropdown, + paymentTermDropdown, + warehouseDropdown, + buildCountryDropdown, + buildRegionDropdown, + productDropdown, +} from '../common'; +import { OroAuth } from '../common/types'; + +export const createOrderAction = createAction({ + auth: oroAuth, + name: 'create_order', + displayName: 'Create Order', + description: 'Creates a new order record in OroCommerce.', + props: { + // -- Required relationships ------------------------------------------------ + customer: customerRequiredDropdown, + customerId: Property.ShortText({ + displayName: 'Customer: Raw ID', + description: 'Overrides the Customer dropdown. Paste an ID from a previous step.', + required: false, + }), + + // -- Optional attributes --------------------------------------------------- + currency: Property.ShortText({ + displayName: 'Currency', + description: 'ISO-4217 3-letter currency code (e.g. USD, EUR).', + required: false, + defaultValue: 'USD', + }), + identifier: Property.ShortText({ + displayName: 'Identifier', + description: 'Unique order reference (e.g. FR1012401Z).', + required: false, + }), + poNumber: Property.ShortText({ + displayName: 'PO Number', + description: 'Purchase order number provided by the buyer.', + required: false, + }), + customerNotes: Property.LongText({ + displayName: 'Customer Notes', + description: 'Notes from the customer (e.g. "Call before delivery").', + required: false, + }), + shipUntil: Property.ShortText({ + displayName: 'Ship Until Date', + description: 'Latest acceptable ship date in YYYY-MM-DD format.', + required: false, + }), + overriddenShippingCostAmount: Property.Number({ + displayName: 'Overridden Shipping Cost', + description: 'Custom shipping cost that overrides the calculated value.', + required: false, + }), + estimatedShippingCostAmount: Property.Number({ + displayName: 'Estimated Shipping Cost', + description: 'Shipping cost calculated from the selected shipping method.', + required: false, + }), + + // -- Optional relationships ------------------------------------------------ + customerUser: customerUserDropdown(false), + customerUserId: Property.ShortText({ + displayName: 'Customer User: Raw ID', + description: 'Overrides the Customer User dropdown.', + required: false, + }), + organization: organizationDropdown, + organizationId: Property.ShortText({ + displayName: 'Organization: Raw ID', + description: 'Overrides the Organization dropdown.', + required: false, + }), + owner: ownerDropdown, + ownerId: Property.ShortText({ + displayName: 'Owner: Raw ID', + description: 'Overrides the Owner dropdown.', + required: false, + }), + website: websiteDropdown, + websiteId: Property.ShortText({ + displayName: 'Website: Raw ID', + description: 'Overrides the Website dropdown.', + required: false, + }), + internalStatus: orderInternalStatusDropdown, + internalStatusId: Property.ShortText({ + displayName: 'Internal Status: Raw ID', + description: 'Overrides the Internal Status dropdown (e.g. order_internal_status.open).', + required: false, + }), + paymentTerm: paymentTermDropdown, + paymentTermId: Property.ShortText({ + displayName: 'Payment Term: Raw ID', + description: 'Overrides the Payment Term dropdown.', + required: false, + }), + warehouse: warehouseDropdown, + warehouseId: Property.ShortText({ + displayName: 'Warehouse: Raw ID', + description: 'Overrides the Warehouse dropdown.', + required: false, + }), + + // -- Billing Address ------------------------------------------------------- + billingAddressLabel: Property.ShortText({ + displayName: 'Billing: Label', + description: 'Address label (e.g. "Main Office").', + required: false, + }), + billingAddressFirstName: Property.ShortText({ + displayName: 'Billing: First Name', + required: false, + }), + billingAddressLastName: Property.ShortText({ + displayName: 'Billing: Last Name', + required: false, + }), + billingAddressOrganization: Property.ShortText({ + displayName: 'Billing: Organization', + required: false, + }), + billingAddressPhone: Property.ShortText({ + displayName: 'Billing: Phone', + required: false, + }), + billingAddressStreet: Property.ShortText({ + displayName: 'Billing: Street', + required: false, + }), + billingAddressStreet2: Property.ShortText({ + displayName: 'Billing: Street 2', + required: false, + }), + billingAddressCity: Property.ShortText({ + displayName: 'Billing: City', + required: false, + }), + billingAddressPostalCode: Property.ShortText({ + displayName: 'Billing: Postal Code', + required: false, + }), + billingAddressCountry: buildCountryDropdown(false, 'Billing: Country'), + billingAddressCountryId: Property.ShortText({ + displayName: 'Billing: Country Raw ID', + description: 'Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).', + required: false, + }), + billingAddressRegion: buildRegionDropdown( + 'billingAddressCountry', + false, + 'Billing: Region / State' + ), + billingAddressRegionId: Property.ShortText({ + displayName: 'Billing: Region Raw ID', + description: 'Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).', + required: false, + }), + billingAddressCustomRegion: Property.ShortText({ + displayName: 'Billing: Custom Region', + description: 'Free-text region for countries without predefined regions.', + required: false, + }), + + // -- Shipping Address ------------------------------------------------------ + shippingAddressLabel: Property.ShortText({ + displayName: 'Shipping: Label', + description: 'Address label (e.g. "Warehouse East").', + required: false, + }), + shippingAddressFirstName: Property.ShortText({ + displayName: 'Shipping: First Name', + required: false, + }), + shippingAddressLastName: Property.ShortText({ + displayName: 'Shipping: Last Name', + required: false, + }), + shippingAddressOrganization: Property.ShortText({ + displayName: 'Shipping: Organization', + required: false, + }), + shippingAddressPhone: Property.ShortText({ + displayName: 'Shipping: Phone', + required: false, + }), + shippingAddressStreet: Property.ShortText({ + displayName: 'Shipping: Street', + required: false, + }), + shippingAddressStreet2: Property.ShortText({ + displayName: 'Shipping: Street 2', + required: false, + }), + shippingAddressCity: Property.ShortText({ + displayName: 'Shipping: City', + required: false, + }), + shippingAddressPostalCode: Property.ShortText({ + displayName: 'Shipping: Postal Code', + required: false, + }), + shippingAddressCountry: buildCountryDropdown(false, 'Shipping: Country'), + shippingAddressCountryId: Property.ShortText({ + displayName: 'Shipping: Country Raw ID', + description: 'Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).', + required: false, + }), + shippingAddressRegion: buildRegionDropdown( + 'shippingAddressCountry', + false, + 'Shipping: Region / State' + ), + shippingAddressRegionId: Property.ShortText({ + displayName: 'Shipping: Region Raw ID', + description: 'Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).', + required: false, + }), + shippingAddressCustomRegion: Property.ShortText({ + displayName: 'Shipping: Custom Region', + description: 'Free-text region for countries without predefined regions.', + required: false, + }), + + // -- Line Items ------------------------------------------------------------ + // DynamicProperties lets us load product units once as StaticDropdown. + // Products are entered as SKU/ID text (framework does not support Dropdown inside Array). + lineItems: Property.DynamicProperties({ + auth: oroAuth, + displayName: 'Line Items', + description: 'Order line items.', + required: true, + refreshers: [], + props: async ({ auth }) => { + type JsonApiCollection = { data: { id: string; attributes: Record }[] }; + const unitOptions: { label: string; value: string }[] = []; + + if (auth) { + try { + const unitsResp = await oroApiCall({ + method: HttpMethod.GET, + resourceUri: '/productunits', + auth: auth as OroAuth, + queryParams: { 'page[size]': '100' }, + }); + for (const item of (unitsResp.body as JsonApiCollection).data ?? []) { + unitOptions.push({ + label: String(item.attributes['label'] || item.id), + value: item.id, + }); + } + } catch { /* leave empty */ } + } + + return { + lineItems: Property.Array({ + displayName: 'Line Items', + required: true, + properties: { + productSku: Property.ShortText({ + displayName: 'Product SKU or ID', + description: 'Enter the product SKU or numeric ID. Use the top-level "Product (Search)" field to look up products.', + required: true, + }), + productUnit: Property.StaticDropdown({ + displayName: 'Product Unit', + description: 'Unit of measurement (e.g. piece, kg, set).', + required: true, + options: { disabled: false, options: unitOptions }, + }), + productUnitId: Property.ShortText({ + displayName: 'Product Unit: Raw ID', + description: 'Overrides the Product Unit dropdown (e.g. "piece", "kg").', + required: false, + }), + quantity: Property.Number({ + displayName: 'Quantity', + required: true, + }), + value: Property.Number({ + displayName: 'Unit Price', + description: 'Price per unit used in this order.', + required: true, + }), + currency: Property.ShortText({ + displayName: 'Currency', + description: 'ISO-4217 code. Defaults to order currency if empty.', + required: false, + }), + comment: Property.ShortText({ + displayName: 'Comment', + required: false, + }), + shipBy: Property.ShortText({ + displayName: 'Ship By Date', + description: 'YYYY-MM-DD. Latest acceptable ship date for this line.', + required: false, + }), + }, + }), + }; + }, + }), + + // -- Product Search helper (top-level, search-enabled) --------------------- + // Use this to find a product SKU/ID, then paste it into line item "Product SKU or ID". + productSearch: productDropdown, + }, + + async run(context) { + const p = context.propsValue; + + // raw ID takes priority over dropdown selection when both are present + const resolve = (rawId: string | null | undefined, dropdownVal: unknown): string | null => { + const raw = rawId?.trim(); + if (raw) return raw; + if (dropdownVal != null && String(dropdownVal).trim()) return String(dropdownVal).trim(); + return null; + }; + + const included: Record[] = []; + const relationships: Record = {}; + + // -- Helper: build an address included resource -------------------------- + function buildAddress( + localId: string, + fields: { + label?: string | null; + firstName?: string | null; + lastName?: string | null; + organization?: string | null; + phone?: string | null; + street?: string | null; + street2?: string | null; + city?: string | null; + postalCode?: string | null; + country?: string | null; + region?: string | null; + customRegion?: string | null; + } + ): boolean { + const hasData = Object.values(fields).some((v) => v != null && v !== ''); + if (!hasData) return false; + + const attrs: Record = { fromExternalSource: false }; + const rels: Record = {}; + + const textFields: Array = [ + 'label', 'firstName', 'lastName', 'organization', + 'phone', 'street', 'street2', 'city', 'postalCode', 'customRegion', + ]; + for (const f of textFields) { + if (fields[f]) attrs[f] = fields[f]; + } + if (fields.country) { + rels['country'] = { data: { type: 'countries', id: fields.country } }; + } + if (fields.region) { + rels['region'] = { data: { type: 'regions', id: fields.region } }; + } + + included.push({ + type: 'orderaddresses', + id: localId, + attributes: attrs, + relationships: rels, + }); + return true; + } + + // -- Billing address ----------------------------------------------------- + const billingAdded = buildAddress('billing_address', { + label: p.billingAddressLabel, + firstName: p.billingAddressFirstName, + lastName: p.billingAddressLastName, + organization: p.billingAddressOrganization, + phone: p.billingAddressPhone, + street: p.billingAddressStreet, + street2: p.billingAddressStreet2, + city: p.billingAddressCity, + postalCode: p.billingAddressPostalCode, + country: resolve(p.billingAddressCountryId, p.billingAddressCountry), + region: resolve(p.billingAddressRegionId, p.billingAddressRegion), + customRegion: p.billingAddressCustomRegion, + }); + if (billingAdded) { + relationships['billingAddress'] = { + data: { type: 'orderaddresses', id: 'billing_address' }, + }; + } + + // -- Shipping address --------------────────────────────────────────────── + const shippingAdded = buildAddress('shipping_address', { + label: p.shippingAddressLabel, + firstName: p.shippingAddressFirstName, + lastName: p.shippingAddressLastName, + organization: p.shippingAddressOrganization, + phone: p.shippingAddressPhone, + street: p.shippingAddressStreet, + street2: p.shippingAddressStreet2, + city: p.shippingAddressCity, + postalCode: p.shippingAddressPostalCode, + country: resolve(p.shippingAddressCountryId, p.shippingAddressCountry), + region: resolve(p.shippingAddressRegionId, p.shippingAddressRegion), + customRegion: p.shippingAddressCustomRegion, + }); + if (shippingAdded) { + relationships['shippingAddress'] = { + data: { type: 'orderaddresses', id: 'shipping_address' }, + }; + } + + // ── Line items ────────────────────────────────────────────────────────── + // DynamicProperties wraps the array in an object keyed by "lineItems" + const dynamicValue = (p.lineItems ?? {}) as Record; + const rawItems = (dynamicValue['lineItems'] ?? []) as Array>; + + const lineItemsRelData = rawItems.map((item, index) => { + const lid = `li_${index + 1}`; + + // productSku holds either a numeric Oro ID or a SKU string + const productSku = (item['productSku'] as string | undefined)?.trim(); + const productUnitId = (item['productUnitId'] as string | undefined)?.trim() + || (item['productUnit'] as string | undefined) + || 'piece'; + + const liAttributes: Record = { + quantity: Number(item['quantity']), + value: Number(item['value']), + currency: item['currency'] || p.currency || 'USD', + priceType: 10, + fromExternalSource: false, + checksum: String(index), + }; + if (item['comment']) liAttributes['comment'] = item['comment']; + if (item['shipBy']) liAttributes['shipBy'] = item['shipBy']; + + const liRelationships: Record = { + productUnit: { data: { type: 'productunits', id: String(productUnitId) } }, + }; + if (productSku) { + liRelationships['product'] = { data: { type: 'products', id: productSku } }; + } + + included.push({ + type: 'orderlineitems', + id: lid, + attributes: liAttributes, + relationships: liRelationships, + }); + + return { type: 'orderlineitems', id: lid }; + }); + relationships['lineItems'] = { data: lineItemsRelData }; + + // ── Order attributes ──────────────────────────────────────────────────── + const attributes: Record = { external: false }; + if (p.currency) attributes['currency'] = p.currency; + if (p.identifier) attributes['identifier'] = p.identifier; + if (p.poNumber) attributes['poNumber'] = p.poNumber; + if (p.customerNotes) attributes['customerNotes'] = p.customerNotes; + if (p.shipUntil) attributes['shipUntil'] = p.shipUntil; + if (p.overriddenShippingCostAmount != null) attributes['overriddenShippingCostAmount'] = p.overriddenShippingCostAmount; + if (p.estimatedShippingCostAmount != null) attributes['estimatedShippingCostAmount'] = p.estimatedShippingCostAmount; + + // ── Order relationships — each resolved via raw ID → dropdown fallback ── + const customerId = resolve(p.customerId, p.customer); + if (customerId) { + relationships['customer'] = { data: { type: 'customers', id: customerId } }; + } + + const customerUserId = resolve(p.customerUserId, p.customerUser); + if (customerUserId) { + relationships['customerUser'] = { data: { type: 'customerusers', id: customerUserId } }; + } + + const organizationId = resolve(p.organizationId, p.organization); + if (organizationId) { + relationships['organization'] = { data: { type: 'organizations', id: organizationId } }; + } + + const ownerId = resolve(p.ownerId, p.owner); + if (ownerId) { + relationships['owner'] = { data: { type: 'users', id: ownerId } }; + } + + const websiteId = resolve(p.websiteId, p.website); + if (websiteId) { + relationships['website'] = { data: { type: 'websites', id: websiteId } }; + } + + const internalStatusId = resolve(p.internalStatusId, p.internalStatus); + if (internalStatusId) { + relationships['internalStatus'] = { data: { type: 'orderinternalstatuses', id: internalStatusId } }; + } + + const paymentTermId = resolve(p.paymentTermId, p.paymentTerm); + if (paymentTermId) { + relationships['paymentTerm'] = { data: { type: 'paymentterms', id: paymentTermId } }; + } + + const warehouseId = resolve(p.warehouseId, p.warehouse); + if (warehouseId) { + relationships['warehouse'] = { data: { type: 'warehouses', id: warehouseId } }; + } + + // ── POST /orders ──────────────────────────────────────────────────────── + const response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: '/orders', + auth: context.auth, + body: { + data: { type: 'orders', attributes, relationships }, + included, + }, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/index.ts b/packages/pieces/community/orocommerce/src/lib/actions/index.ts new file mode 100644 index 000000000000..5d07c848e973 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/index.ts @@ -0,0 +1,3 @@ +export { createInvoiceAction } from './create-invoice'; +export { createOrderAction } from './create-order'; + diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 7834da5a5182..9d992d8e2f6a 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -1,9 +1,9 @@ import { PieceAuth, Property } from '@activepieces/pieces-framework'; import { HttpMethod } from '@activepieces/pieces-common'; -import { oroCommerceApiCall } from './client'; +import { oroApiCall } from './client'; import { AppConnectionType } from '@activepieces/shared'; -export const orocommerceAuth = PieceAuth.CustomAuth({ +export const oroAuth = PieceAuth.CustomAuth({ description: ` Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. @@ -46,7 +46,7 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. validate: async ({ auth }) => { try { - await oroCommerceApiCall({ + await oroApiCall({ method: HttpMethod.GET, resourceUri: '/regions/US-CA', auth: { diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index af4b5a2081f1..5c8523efe8f3 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -7,20 +7,20 @@ import { } from '@activepieces/pieces-common'; import { - type OroCommerceAuth, - type OroCommerceAuthResponseType, - type OroCommerceApiCallParams, + type OroAuth, + type OroAuthResponseType, + type OroApiCallParams, } from './types'; let cachedToken: string | null = null; let tokenExpiresAt = 0; -async function getAccessToken(auth: OroCommerceAuth): Promise { +async function getAccessToken(auth: OroAuth): Promise { if (cachedToken && Date.now() < tokenExpiresAt) { return cachedToken; } - const response = await httpClient.sendRequest({ + const response = await httpClient.sendRequest({ method: HttpMethod.POST, url: auth.props.serverUrl + '/oauth2-token', headers: { @@ -39,13 +39,13 @@ async function getAccessToken(auth: OroCommerceAuth): Promise { return cachedToken; } -export async function oroCommerceApiCall({ +export async function oroApiCall({ method, resourceUri, auth, queryParams, body, -}: OroCommerceApiCallParams): Promise> { +}: OroApiCallParams): Promise> { try { return await httpClient.sendRequest({ method, diff --git a/packages/pieces/community/orocommerce/src/lib/common/index.ts b/packages/pieces/community/orocommerce/src/lib/common/index.ts index 9f3087bf9e32..8e86756d7ebf 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -1,3 +1,4 @@ export * from './auth'; export * from './client'; export * from './register-webhook'; +export * from './props'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts new file mode 100644 index 000000000000..34d65eba75f1 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -0,0 +1,546 @@ +import { Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { oroAuth } from './auth'; +import { oroApiCall } from './client'; +import { OroAuth } from './types'; + +// --- Internal types ----------------------------------------------------------- + +interface OroJsonApiItem { + id: string; + type: string; + attributes: Record; +} + +interface OroJsonApiCollection { + data: OroJsonApiItem[]; +} + +// --- Helpers ------------------------------------------------------------------ + +const NOT_CONNECTED = { + disabled: true, + placeholder: 'Connect your OroCommerce account first', + options: [], +}; + +const FAILED = { + disabled: true, + placeholder: 'Failed to load options. Check your connection.', + options: [], +}; + +async function fetchCollection( + auth: OroAuth, + resourceUri: string, + queryParams?: Record +): Promise { + const response = await oroApiCall({ + method: HttpMethod.GET, + resourceUri, + auth, + queryParams: { 'page[size]': '50', ...queryParams }, + }); + + return (response.body as OroJsonApiCollection).data ?? []; +} + +// --- Customers ---------------------------------------------------------------- +// Supports filter[searchText] — search across name and other indexed fields. + +function buildCustomerOptions( + required: boolean, + displayName = 'Customer', + description = 'Select a customer.' +) { + return Property.Dropdown({ + auth: oroAuth, + displayName, + description, + required, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[customers]': 'id,name', + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; + } + + const items = await fetchCollection( + auth as OroAuth, + '/customers', + params + ); + return { + options: items.map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, + }); +} + +export const customerDropdown = buildCustomerOptions(false); +export const customerRequiredDropdown = buildCustomerOptions( + true, + 'Customer', + 'The customer this order belongs to.' +); + +// --- Customer Users ----------------------------------------------------------- +// Supports filter[searchText] — searches email, firstName, lastName, etc. +// Also pre-filters by customer relationship when customer is selected. + +export const customerUserDropdown = (required = false) => + Property.Dropdown({ + auth: oroAuth, + displayName: 'Customer User', + description: 'Select a Customer User.', + required, + refreshers: ['customer', 'customerId'], + refreshOnSearch: true, + options: async ({ auth, customer, customerId }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + + try { + const params: Record = { + 'fields[customerusers]': 'id,firstName,lastName,email', + }; + + const resolvedCustomer = (customerId as string) || (customer as string); + if (!resolvedCustomer || resolvedCustomer.length == 0) { + return { + disabled: true, + placeholder: 'Select Customer first.', + options: [], + }; + } + const searchFilters = [`customer_id = ${resolvedCustomer}`]; + if (searchValue && searchValue.trim().length > 0) { + searchFilters.push(`allText ~ "${searchValue.trim().replace('"', '')}"`); + } + + params['filter[searchQuery]'] = searchFilters.join(' and '); + const items = await fetchCollection( + auth as OroAuth, + '/customerusers', + params + ); + return { + options: items.map((item) => { + const firstName = String(item.attributes['firstName'] ?? ''); + const lastName = String(item.attributes['lastName'] ?? ''); + const email = String(item.attributes['email'] ?? ''); + const label = + [firstName, lastName].filter(Boolean).join(' ') + ' - ' + email || + item.id; + return { label, value: item.id }; + }), + }; + } catch { + return FAILED; + } + }, + }); + +// --- Organizations ------------------------------------------------------------ +// Supports filter[searchText]. + +export const organizationDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Organization', + description: 'Search organizations.', + required: false, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[organizations]': 'id,name' + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; + } + + const items = await fetchCollection( + auth as OroAuth, + '/organizations', + params + ); + return { + options: items.map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Owner (admin users) ------------------------------------------------------ +// Supports filter[searchText] — searches username, email, firstName, lastName. + +export const ownerDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Owner', + description: + 'Search user by name, username or email.', + required: false, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[users]': 'id,firstName,lastName,username' + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; + } + const items = await fetchCollection(auth as OroAuth, '/users', params); + return { + options: items.map((item) => { + const firstName = String(item.attributes['firstName'] ?? ''); + const lastName = String(item.attributes['lastName'] ?? ''); + const username = String(item.attributes['username'] ?? ''); + const label = + [firstName, lastName].filter(Boolean).join(' ') || + username || + item.id; + return { label, value: item.id }; + }), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Websites ----------------------------------------------------------------- +// Supports filter[searchText]. Small set in practice — still search-gated. + +export const websiteDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Website', + description: 'Search websites.', + required: false, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[websites]': 'id,name' + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; + } + const items = await fetchCollection(auth as OroAuth, '/websites', params); + return { + options: items.map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Internal Statuses (Invoice) ---------------------------------------------- +// Small enum — loaded once, no search needed. + +export const invoiceInternalStatusDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Internal Status', + description: 'Invoice internal status (e.g. Draft, Open).', + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) return NOT_CONNECTED; + try { + const items = await fetchCollection( + auth as OroAuth, + '/invoiceinternalstatuses' + ); + return { + options: items.map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Order Internal Statuses -------------------------------------------------- +// Small enum — loaded once, no search needed. + +export const orderInternalStatusDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Internal Status', + description: 'Order internal status (e.g. Open, Cancelled).', + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) return NOT_CONNECTED; + try { + const items = await fetchCollection( + auth as OroAuth, + '/orderinternalstatuses' + ); + return { + options: items.map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Products ---────────────────────────────────────────────────────────────── +// Supports filter[searchText] — searches SKU, names, descriptions, etc. + +export const productDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Product', + description: 'Search products.', + required: false, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[products]': 'sku,status,names', + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}" and productStatus = "enabled"`; + } else { + params['filter[status]'] = 'enabled'; + } + + const items = await fetchCollection(auth as OroAuth, '/products', params); + return { + options: items.map((item) => ({ + label: String(item.attributes['sku'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// ─── Payment Terms ──────────────────────────────────────────────────────────── +// Supports filter[searchText]. + +export const paymentTermDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Payment Term', + description: 'Search payment terms.', + required: false, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[paymentterms]': 'id,label' + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `label ~ "${searchValue.trim().replace('"', '')}"`; + } + + const items = await fetchCollection( + auth as OroAuth, + '/paymentterms', + params + ); + return { + options: items.map((item) => ({ + label: String(item.attributes['label'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// ─── Warehouses ─────────────────────────────────────────────────────────────── +// Supports filter[searchText]. + +export const warehouseDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Warehouse', + description: 'Search warehouses.', + required: false, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[warehouses]': 'id,name' + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; + } + const items = await fetchCollection( + auth as OroAuth, + '/warehouses', + params + ); + return { + options: items.map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// ─── Countries ──────────────────────────────────────────────────────────────── +// Static dictionary — load full list once per connection (cached by auth refresher). +// Uses filter[name] for client-side filtering via the UI's built-in search box. +// Page size 300 to capture all ~250 ISO countries in one request. + +export const buildCountryDropdown = (required = false, displayName = 'Country') => + Property.Dropdown({ + auth: oroAuth, + displayName, + description: 'ISO-3166 country. Start typing to filter the list.', + required, + refreshers: [], + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[countries]': 'id,name' + }; + // if (searchValue && searchValue.trim().length > 0) { + // params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; + // } + + const items = await fetchCollection( + auth as OroAuth, + '/countries', + params + ); + return { + options: items + .map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, // ISO2 code e.g. "US" + })) + .sort((a, b) => a.label.localeCompare(b.label)), + }; + } catch { + return FAILED; + } + }, + }); + +// ─── Regions ────────────────────────────────────────────────────────────────── +// Static dictionary per country — load all regions for the selected country once. +// Cached by auth + countryRefresher. Uses filter[country] to scope results. + +export const buildRegionDropdown = ( + countryRefresher: string, + required = false, + displayName = 'Region / State' +) => + Property.Dropdown({ + auth: oroAuth, + displayName, + description: + 'Region or state. Select a country first. Start typing to filter.', + required, + refreshers: [countryRefresher], + options: async (context) => { + const auth = context['auth'] as OroAuth | undefined; + const searchValue = context['searchValue'] as string | undefined; + const countryId = context[countryRefresher as keyof typeof context] as + | string + | undefined; + if (!auth) return NOT_CONNECTED; + if (!countryId) + return { + disabled: true, + placeholder: 'Select a country first', + options: [], + }; + try { + const params: Record = {'filter[country]': countryId}; + // if (searchValue && searchValue.trim().length > 0) { + // params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; + // params['filter[name]'] = searchValue.trim(); + // } + + const items = await fetchCollection(auth, '/regions', params); + return { + options: items + .map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, // ISO 3166-2 code e.g. "US-NY" + })) + .sort((a, b) => a.label.localeCompare(b.label)), + }; + } catch { + return FAILED; + } + }, + }); + +// ─── Product Units ──────────────────────────────────────────────────────────── +// Small static list (each, set, kg, …) — load all once per connection. + +export const productUnitDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Product Unit', + description: 'Unit of measure for the product (e.g. each, set, kg).', + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) return NOT_CONNECTED; + try { + const items = await fetchCollection(auth as OroAuth, '/productunits', { + 'page[size]': '100', + }); + return { + options: items.map((item) => ({ + // productunits use the string code as id (e.g. "each"), attributes may have label + label: String( + item.attributes['label'] ?? item.attributes['code'] ?? item.id + ), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts index 98c698f828f5..97cd08297a58 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts @@ -3,11 +3,11 @@ import { Trigger, TriggerStrategy, } from '@activepieces/pieces-framework'; -import { orocommerceAuth } from './auth'; -import { oroCommerceApiCall } from './client'; +import { oroAuth } from './auth'; +import { oroApiCall } from './client'; import { HttpMethod } from '@activepieces/pieces-common'; -export const createOroCommerceWebhookTrigger = ({ +export const createOroWebhookTrigger = ({ name, description, displayName, @@ -23,7 +23,7 @@ export const createOroCommerceWebhookTrigger = ({ sampleData: Record; }): Trigger => createTrigger({ - auth: orocommerceAuth, + auth: oroAuth, name, description, displayName, @@ -32,13 +32,13 @@ export const createOroCommerceWebhookTrigger = ({ type: TriggerStrategy.WEBHOOK, async onEnable(context) { - const response = await oroCommerceApiCall({ + const response = await oroApiCall({ method: HttpMethod.POST, - resourceUri: '/remotenotifications', + resourceUri: '/remotewebhooks', auth: context.auth, body: { data: { - type: 'remotenotifications', + type: 'remotewebhooks', attributes: { channel: topic, event, @@ -65,9 +65,9 @@ export const createOroCommerceWebhookTrigger = ({ ); if (webhookInfo !== null && webhookInfo !== undefined) { - await oroCommerceApiCall({ + await oroApiCall({ method: HttpMethod.DELETE, - resourceUri: `/remotenotifications/${webhookInfo.webhookId}`, + resourceUri: `/remotewebhooks/${webhookInfo.webhookId}`, auth: context.auth, }); } diff --git a/packages/pieces/community/orocommerce/src/lib/common/types.ts b/packages/pieces/community/orocommerce/src/lib/common/types.ts index 2585a2c31af4..960fca0ef2f9 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/types.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -6,20 +6,20 @@ import { import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework'; -import { orocommerceAuth } from './auth'; +import { oroAuth } from './auth'; -export type OroCommerceAuth = AppConnectionValueForAuthProperty; +export type OroAuth = AppConnectionValueForAuthProperty; -export type OroCommerceAuthResponseType = { +export type OroAuthResponseType = { token_type: string; access_token: string; expires_in: number; }; -export type OroCommerceApiCallParams = { +export type OroApiCallParams = { method: HttpMethod; resourceUri: string; - auth: OroCommerceAuth; + auth: OroAuth; queryParams?: QueryParams; body?: HttpMessageBody; }; diff --git a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts new file mode 100644 index 000000000000..fec4383619b3 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts @@ -0,0 +1,296 @@ +export const invoiceCreateData = { + event: 'create', + channel: 'invoice', + data: { + type: 'invoices', + id: '1', + attributes: { + uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', + invoiceNumber: 'INV-2026-02-00001', + invoiceDate: '2026-02-03', + currency: 'USD', + totalAmount: '59.2100', + customerName: 'Customer A', + refCustomerId: null, + refCustomerUserId: null, + title: 'Sample Invoice', + description: 'Sample prepaid service', + memo: 'Thank you for choosing us!', + billTo: '101 Pine St, Ogdenville, USA, 44556', + shipTo: '654 Elm St, Shelbyville, USA, 09876', + shippingMethod: 'International Shipping (Tracking #: 901234)', + sellerInfo: + 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', + externalPaymentUrl: 'https://example.com/pay/123456790', + createdAt: '2026-02-03T11:33:49Z', + updatedAt: '2026-02-18T11:35:37Z', + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: 0, + timesContactedIn: 0, + timesContactedOut: 0, + paymentStatus: { + code: 'refunded', + label: 'Refunded', + isForced: false, + }, + }, + relationships: { + lineItems: { + data: [ + { + type: 'invoicelineitems', + id: '1', + }, + { + type: 'invoicelineitems', + id: '2', + }, + { + type: 'invoicelineitems', + id: '3', + }, + ], + }, + owner: { + data: { + type: 'users', + id: '1', + }, + }, + organization: { + data: { + type: 'organizations', + id: '1', + }, + }, + customer: { + data: null, + }, + customer_user: { + data: null, + }, + website: { + data: null, + }, + invoiceDefaultPdfFile: { + data: null, + }, + orders: { + data: [], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + internal_status: { + data: { + type: 'invoiceinternalstatuses', + id: 'draft', + }, + }, + }, + }, + timestamp: 1771523549, +}; + +export const invoiceUpdateData = { + event: 'update', + channel: 'invoice', + data: { + type: 'invoices', + id: '1', + attributes: { + uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', + invoiceNumber: 'INV-2026-02-00001', + invoiceDate: '2026-02-03', + currency: 'USD', + totalAmount: '59.2100', + customerName: 'Customer A', + refCustomerId: null, + refCustomerUserId: null, + title: 'Sample Invoice', + description: 'Sample prepaid service', + memo: 'Thank you for choosing us!', + billTo: '101 Pine St, Ogdenville, USA, 44556', + shipTo: '654 Elm St, Shelbyville, USA, 09876', + shippingMethod: 'International Shipping (Tracking #: 901234)', + sellerInfo: + 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', + externalPaymentUrl: 'https://example.com/pay/123456790', + createdAt: '2026-02-03T11:33:49Z', + updatedAt: '2026-02-18T11:35:37Z', + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: 0, + timesContactedIn: 0, + timesContactedOut: 0, + paymentStatus: { + code: 'refunded', + label: 'Refunded', + isForced: false, + }, + }, + relationships: { + lineItems: { + data: [ + { + type: 'invoicelineitems', + id: '1', + }, + { + type: 'invoicelineitems', + id: '2', + }, + { + type: 'invoicelineitems', + id: '3', + }, + ], + }, + owner: { + data: { + type: 'users', + id: '1', + }, + }, + organization: { + data: { + type: 'organizations', + id: '1', + }, + }, + customer: { + data: null, + }, + customer_user: { + data: null, + }, + website: { + data: null, + }, + invoiceDefaultPdfFile: { + data: null, + }, + orders: { + data: [], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + internal_status: { + data: { + type: 'invoiceinternalstatuses', + id: 'draft', + }, + }, + }, + }, + timestamp: 1771527786, +}; + +export const invoiceDeleteData = { + event: 'delete', + channel: 'invoice', + data: { + type: 'invoices', + id: '1', + attributes: { + uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', + invoiceNumber: 'INV-2026-02-00001', + invoiceDate: '2026-02-03', + currency: 'USD', + totalAmount: '59.2100', + customerName: 'Customer A', + refCustomerId: null, + refCustomerUserId: null, + title: 'Sample Invoice', + description: 'Sample prepaid service', + memo: 'Thank you for choosing us!', + billTo: '101 Pine St, Ogdenville, USA, 44556', + shipTo: '654 Elm St, Shelbyville, USA, 09876', + shippingMethod: 'International Shipping (Tracking #: 901234)', + sellerInfo: + 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', + externalPaymentUrl: 'https://example.com/pay/123456790', + createdAt: '2026-02-03T11:33:49Z', + updatedAt: '2026-02-18T11:35:37Z', + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: 0, + timesContactedIn: 0, + timesContactedOut: 0, + paymentStatus: { + code: 'refunded', + label: 'Refunded', + isForced: false, + }, + }, + relationships: { + lineItems: { + data: [ + { + type: 'invoicelineitems', + id: '1', + }, + { + type: 'invoicelineitems', + id: '2', + }, + { + type: 'invoicelineitems', + id: '3', + }, + ], + }, + owner: { + data: { + type: 'users', + id: '1', + }, + }, + organization: { + data: { + type: 'organizations', + id: '1', + }, + }, + customer: { + data: null, + }, + customer_user: { + data: null, + }, + website: { + data: null, + }, + invoiceDefaultPdfFile: { + data: null, + }, + orders: { + data: [], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + internal_status: { + data: { + type: 'invoiceinternalstatuses', + id: 'draft', + }, + }, + }, + }, + timestamp: 1771527301, +}; diff --git a/packages/pieces/community/orocommerce/src/lib/examples/order.ts b/packages/pieces/community/orocommerce/src/lib/examples/order.ts new file mode 100644 index 000000000000..799c916e5410 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/examples/order.ts @@ -0,0 +1,628 @@ +export const orderCreateData = { + event: 'create', + channel: 'order', + data: { + type: 'orders', + id: '48', + attributes: { + identifier: '48', + poNumber: 'PONUM111222334', + customerNotes: 'Customer Notes', + shipUntil: { + date: '2026-02-27 00:00:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + currency: 'USD', + subtotalWithDiscountsValue: '79.9900', + subtotalWithDiscounts: '79.9900', + subtotalValue: '79.9900', + totalValue: '89.9900', + shippingMethod: 'fixed_product_6', + shippingMethodType: 'primary', + estimatedShippingCostAmount: '10.0000', + overriddenShippingCostAmount: null, + sourceEntityIdentifier: null, + totalDiscountsAmount: null, + external: false, + createdAt: { + date: '2026-02-19 19:40:25.000000', + timezone_type: 3, + timezone: 'UTC', + }, + updatedAt: { + date: '2026-02-19 19:40:25.000000', + timezone_type: 3, + timezone: 'UTC', + }, + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: null, + timesContactedIn: null, + timesContactedOut: null, + disablePromotions: false, + discount: 0, + shippingDiscount: 0, + }, + relationships: { + createdBy: { + data: { + type: 'users', + id: '1', + }, + }, + billingAddress: { + data: { + type: 'orderaddresses', + id: '86', + }, + }, + shippingAddress: { + data: { + type: 'orderaddresses', + id: '87', + }, + }, + website: { + data: { + type: 'websites', + id: '1', + }, + }, + lineItems: { + data: [ + { + type: 'orderlineitems', + id: '129', + }, + ], + }, + discounts: { + data: [], + }, + shippingTrackings: { + data: [], + }, + parent: { + data: null, + }, + subOrders: { + data: [], + }, + owner: { + data: { + type: 'users', + id: '1', + }, + }, + organization: { + data: { + type: 'organizations', + id: '1', + }, + }, + customerUser: { + data: { + type: 'customerusers', + id: '1', + }, + }, + customer: { + data: { + type: 'customers', + id: '5', + }, + }, + paymentTerm: { + data: { + type: 'paymentterms', + id: '1', + }, + }, + invoices: { + data: [], + }, + warehouse: { + data: { + type: 'warehouses', + id: '1', + }, + }, + source: { + data: null, + }, + internalStatus: { + data: { + type: 'orderinternalstatuses', + id: 'order_internal_status.open', + }, + }, + orderSubtotals: { + data: [ + { + type: 'ordersubtotals', + id: '48-subtotal-0', + }, + { + type: 'ordersubtotals', + id: '48-discount-1', + }, + { + type: 'ordersubtotals', + id: '48-shipping_cost-2', + }, + { + type: 'ordersubtotals', + id: '48-discount-3', + }, + { + type: 'ordersubtotals', + id: '48-tax-4', + }, + { + type: 'ordersubtotals', + id: '48-tax-5', + }, + { + type: 'ordersubtotals', + id: '48-tax-6', + }, + ], + }, + attachments: { + data: [], + }, + documents: { + data: [ + { + type: 'files', + id: '661', + meta: { + sortOrder: 1, + }, + }, + ], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + status: { + data: null, + }, + shippingStatus: { + data: { + type: 'ordershippingstatuses', + id: 'order_shipping_status.not_shipped', + }, + }, + }, + }, + timestamp: 1771530026, +}; + +export const orderUpdateData = { + event: 'update', + channel: 'order', + data: { + type: 'orders', + id: '42', + attributes: { + identifier: '42', + poNumber: 'PONUM111222334', + customerNotes: 'Order Customer Notes', + shipUntil: { + date: '2026-02-28 00:00:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + currency: 'USD', + subtotalWithDiscountsValue: '236.9700', + subtotalWithDiscounts: '236.9700', + subtotalValue: '239.9700', + totalValue: '246.9700', + shippingMethod: 'fixed_product_6', + shippingMethodType: 'primary', + estimatedShippingCostAmount: '10.0000', + overriddenShippingCostAmount: null, + sourceEntityIdentifier: null, + totalDiscountsAmount: '3.0000', + external: false, + createdAt: { + date: '2026-02-19 17:52:28.000000', + timezone_type: 3, + timezone: 'UTC', + }, + updatedAt: { + date: '2026-02-19 19:03:06.000000', + timezone_type: 3, + timezone: 'UTC', + }, + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: null, + timesContactedIn: null, + timesContactedOut: null, + disablePromotions: false, + discount: 0, + shippingDiscount: 0, + }, + relationships: { + createdBy: { + data: { + type: 'users', + id: '1', + }, + }, + billingAddress: { + data: { + type: 'orderaddresses', + id: '74', + }, + }, + shippingAddress: { + data: { + type: 'orderaddresses', + id: '75', + }, + }, + website: { + data: { + type: 'websites', + id: '1', + }, + }, + lineItems: { + data: [ + { + type: 'orderlineitems', + id: '123', + }, + ], + }, + discounts: { + data: [ + { + type: 'orderdiscounts', + id: '1', + }, + ], + }, + shippingTrackings: { + data: [], + }, + parent: { + data: null, + }, + subOrders: { + data: [], + }, + owner: { + data: { + type: 'users', + id: '1', + }, + }, + organization: { + data: { + type: 'organizations', + id: '1', + }, + }, + customerUser: { + data: { + type: 'customerusers', + id: '1', + }, + }, + customer: { + data: { + type: 'customers', + id: '5', + }, + }, + paymentTerm: { + data: { + type: 'paymentterms', + id: '1', + }, + }, + invoices: { + data: [], + }, + warehouse: { + data: { + type: 'warehouses', + id: '1', + }, + }, + source: { + data: null, + }, + internalStatus: { + data: { + type: 'orderinternalstatuses', + id: 'order_internal_status.open', + }, + }, + orderSubtotals: { + data: [ + { + type: 'ordersubtotals', + id: '42-subtotal-0', + }, + { + type: 'ordersubtotals', + id: '42-discount-1', + }, + { + type: 'ordersubtotals', + id: '42-discount-2', + }, + { + type: 'ordersubtotals', + id: '42-shipping_cost-3', + }, + { + type: 'ordersubtotals', + id: '42-discount-4', + }, + { + type: 'ordersubtotals', + id: '42-tax-5', + }, + { + type: 'ordersubtotals', + id: '42-tax-6', + }, + { + type: 'ordersubtotals', + id: '42-tax-7', + }, + ], + }, + attachments: { + data: [], + }, + documents: { + data: [ + { + type: 'files', + id: '656', + meta: { + sortOrder: 1, + }, + }, + ], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + status: { + data: null, + }, + shippingStatus: { + data: { + type: 'ordershippingstatuses', + id: 'order_shipping_status.not_shipped', + }, + }, + }, + }, + timestamp: 1771527786, +}; + +export const orderDeleteData = { + event: 'delete', + channel: 'order', + data: { + type: 'orders', + id: '20', + attributes: { + identifier: 'FR1012408', + poNumber: 'RT104568EUR', + customerNotes: null, + shipUntil: { + date: '2026-02-18 00:00:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + currency: 'USD', + subtotalWithDiscountsValue: '33951.0000', + subtotalWithDiscounts: '33951.0000', + subtotalValue: '33951.0000', + totalValue: '33951.0000', + shippingMethod: null, + shippingMethodType: null, + estimatedShippingCostAmount: null, + overriddenShippingCostAmount: null, + sourceEntityIdentifier: null, + totalDiscountsAmount: null, + external: false, + createdAt: { + date: '2025-12-19 21:04:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + updatedAt: { + date: '2026-02-18 11:35:37.000000', + timezone_type: 3, + timezone: 'UTC', + }, + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: 0, + timesContactedIn: 0, + timesContactedOut: 0, + disablePromotions: false, + discount: 0, + shippingDiscount: 0, + }, + relationships: { + createdBy: { + data: { + type: 'users', + id: '4', + }, + }, + billingAddress: { + data: { + type: 'orderaddresses', + id: '27', + }, + }, + shippingAddress: { + data: { + type: 'orderaddresses', + id: '28', + }, + }, + website: { + data: { + type: 'websites', + id: '1', + }, + }, + lineItems: { + data: [ + { + type: 'orderlineitems', + id: '65', + }, + { + type: 'orderlineitems', + id: '66', + }, + { + type: 'orderlineitems', + id: '67', + }, + ], + }, + discounts: { + data: [], + }, + shippingTrackings: { + data: [], + }, + parent: { + data: null, + }, + subOrders: { + data: [], + }, + owner: { + data: { + type: 'users', + id: '21', + }, + }, + organization: { + data: { + type: 'organizations', + id: '1', + }, + }, + customerUser: { + data: { + type: 'customerusers', + id: '11', + }, + }, + customer: { + data: { + type: 'customers', + id: '11', + }, + }, + paymentTerm: { + data: { + type: 'paymentterms', + id: '3', + }, + }, + invoices: { + data: [ + { + type: 'invoices', + id: '20', + }, + ], + }, + warehouse: { + data: null, + }, + source: { + data: null, + }, + internalStatus: { + data: { + type: 'orderinternalstatuses', + id: 'order_internal_status.open', + }, + }, + orderSubtotals: { + data: [ + { + type: 'ordersubtotals', + id: '20-subtotal-0', + }, + { + type: 'ordersubtotals', + id: '20-discount-1', + }, + { + type: 'ordersubtotals', + id: '20-shipping_cost-2', + }, + { + type: 'ordersubtotals', + id: '20-discount-3', + }, + { + type: 'ordersubtotals', + id: '20-tax-4', + }, + { + type: 'ordersubtotals', + id: '20-tax-5', + }, + { + type: 'ordersubtotals', + id: '20-tax-6', + }, + ], + }, + attachments: { + data: [], + }, + documents: { + data: [], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + status: { + data: null, + }, + shippingStatus: { + data: { + type: 'ordershippingstatuses', + id: 'order_shipping_status.not_shipped', + }, + }, + }, + }, + timestamp: 1771527301, +}; diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts b/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts new file mode 100644 index 000000000000..28ad13ba6ad1 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts @@ -0,0 +1,29 @@ +import { createOroWebhookTrigger } from '../common/register-webhook'; +import { invoiceCreateData, invoiceUpdateData, invoiceDeleteData } from '../examples/invoice'; + +export const newInvoice = createOroWebhookTrigger({ + name: 'new_invoice', + description: 'Triggered when a new invoice is created', + topic: 'invoice', + event: 'create', + displayName: 'New Invoice', + sampleData: invoiceCreateData, +}); + +export const updatedInvoice = createOroWebhookTrigger({ + name: 'updated_invoice', + description: 'Triggered when an invoice is updated', + topic: 'invoice', + event: 'update', + displayName: 'Invoice Update', + sampleData: invoiceUpdateData, +}); + +export const removedInvoice = createOroWebhookTrigger({ + name: 'removed_invoice', + description: 'Triggered when an invoice is deleted', + topic: 'invoice', + event: 'delete', + displayName: 'Invoice Removal', + sampleData: invoiceDeleteData, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts b/packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts deleted file mode 100644 index 346acf2f4baa..000000000000 --- a/packages/pieces/community/orocommerce/src/lib/triggers/new-order.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { createOroCommerceWebhookTrigger } from '../common/register-webhook'; - -export const newOrder = createOroCommerceWebhookTrigger({ - name: 'new_order', - description: 'Triggered when a new order is created', - topic: 'order', - event: 'create', - displayName: 'New Order', - sampleData: { - "event": "create", - "channel": "order", - "data": { - "id": 26, - "identifier": "26", - "poNumber": "PONUM111", - "customerNotes": "Some notes", - "shipUntil": { - "date": "2025-11-28 00:00:00.000000", - "timezone_type": 3, - "timezone": "UTC" - }, - "currency": "USD", - "subtotalWithDiscountsCurrency": "USD", - "subtotalWithDiscountsValue": 788.95, - "subtotalWithDiscounts": 788.95, - "subtotalCurrency": "USD", - "subtotalValue": 808.95, - "baseSubtotalValue": 808.95, - "totalCurrency": "USD", - "totalValue": 798.95, - "baseTotalValue": 798.95, - "shippingMethod": "flat_rate_6", - "shippingMethodType": "primary", - "estimatedShippingCostAmount": 10, - "overriddenShippingCostAmount": null, - "sourceEntityClass": "Oro\\Bundle\\ShoppingListBundle\\Entity\\ShoppingList", - "sourceEntityId": 1, - "sourceEntityIdentifier": "Care Team Reorder", - "totalDiscountsAmount": null, - "external": false, - "createdAt": { - "date": "2025-11-05 23:15:25.000000", - "timezone_type": 3, - "timezone": "UTC" - }, - "updatedAt": { - "date": "2025-11-05 23:15:25.000000", - "timezone_type": 3, - "timezone": "UTC" - }, - "ac_last_contact_date": null, - "ac_last_contact_date_in": null, - "ac_last_contact_date_out": null, - "ac_contact_count": null, - "ac_contact_count_in": null, - "ac_contact_count_out": null, - "serialized_data": { - "internal_status": "order_internal_status.open", - "shippingStatus": "order_shipping_status.not_shipped" - }, - "disablePromotions": false - }, - "timestamp": 1762384555 - } -}); diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/order.ts b/packages/pieces/community/orocommerce/src/lib/triggers/order.ts new file mode 100644 index 000000000000..de67d171f13a --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/triggers/order.ts @@ -0,0 +1,29 @@ +import { createOroWebhookTrigger } from '../common/register-webhook'; +import { orderCreateData, orderUpdateData, orderDeleteData } from '../examples/order'; + +export const newOrder = createOroWebhookTrigger({ + name: 'new_order', + description: 'Triggered when a new order is created', + topic: 'order', + event: 'create', + displayName: 'New Order', + sampleData: orderCreateData, +}); + +export const updatedOrder = createOroWebhookTrigger({ + name: 'updated_order', + description: 'Triggered when an order is updated', + topic: 'order', + event: 'update', + displayName: 'Order Update', + sampleData: orderUpdateData, +}); + +export const removedOrder = createOroWebhookTrigger({ + name: 'removed_order', + description: 'Triggered when an order is deleted', + topic: 'order', + event: 'delete', + displayName: 'Order Removal', + sampleData: orderDeleteData, +}); From 44483dae30282ef9c252be1c3f97b2b9a8cf654f Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 23 Feb 2026 18:37:32 +0100 Subject: [PATCH 04/50] feat(orocommerce): improved order and invoice actions --- .../src/lib/actions/create-invoice.ts | 74 +------ .../src/lib/actions/create-order.ts | 177 ++++++++++++++--- .../orocommerce/src/lib/common/client.ts | 17 ++ .../orocommerce/src/lib/common/props.ts | 182 ++++++++++++------ .../orocommerce/src/lib/common/types.ts | 10 + 5 files changed, 316 insertions(+), 144 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index 7060cdb58a87..aef4138377ba 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -8,9 +8,8 @@ import { invoiceInternalStatusDropdown, organizationDropdown, ownerDropdown, - websiteDropdown + websiteDropdown, } from '../common'; -import { OroAuth } from '../common/types'; export const createInvoiceAction = createAction({ auth: oroAuth, @@ -48,13 +47,13 @@ export const createInvoiceAction = createAction({ description: 'Overrides the Customer User dropdown.', required: false, }), - refCustomerId: Property.ShortText({ + refCustomerId: Property.Number({ displayName: 'External Customer ID', description: 'An optional ID reference to a customer. Can be used for storing an arbitrary external ID.', required: false, }), - refCustomerUserId: Property.ShortText({ + refCustomerUserId: Property.Number({ displayName: 'External Customer User ID', description: 'An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.', @@ -151,32 +150,7 @@ export const createInvoiceAction = createAction({ 'Invoice line items. Each item is sent via JSON:API included.', required: true, refreshers: [], - props: async ({ auth }) => { - type JsonApiCollection = { - data: { id: string; attributes: Record }[]; - }; - const unitOptions: { label: string; value: string }[] = []; - - if (auth) { - try { - const unitsResp = await oroApiCall({ - method: HttpMethod.GET, - resourceUri: '/productunits', - auth: auth as OroAuth, - queryParams: { 'page[size]': '100' }, - }); - for (const item of (unitsResp.body as JsonApiCollection).data ?? - []) { - unitOptions.push({ - label: String(item.attributes['label'] || item.id), - value: item.id, - }); - } - } catch { - /* leave empty */ - } - } - + props: async () => { return { lineItems: Property.Array({ displayName: 'Line Items', @@ -192,29 +166,16 @@ export const createInvoiceAction = createAction({ description: 'Line item description (HTML allowed).', required: true, }), - productSku: Property.ShortText({ - displayName: 'Product SKU or ID', - description: - 'Enter the product SKU or numeric ID. Use the top-level "Product (Search)" field to look up products.', - required: false, - }), - productUnit: Property.StaticDropdown({ - displayName: 'Product Unit', - description: 'Unit of measurement (e.g. piece, kg, set).', - required: false, - options: { disabled: false, options: unitOptions }, - }), - productUnitId: Property.ShortText({ - displayName: 'Product Unit: Raw ID', - description: - 'Overrides the Product Unit dropdown (e.g. "piece", "kg").', - required: false, - }), quantity: Property.Number({ displayName: 'Quantity', description: 'Quantity of the item.', required: true, }), + unitOfQuantity: Property.ShortText({ + displayName: 'Product Unit', + description: 'Unit of measurement (e.g. piece, kg, set).', + required: false, + }), unitPrice: Property.Number({ displayName: 'Unit Price', description: 'Price per unit.', @@ -261,36 +222,21 @@ export const createInvoiceAction = createAction({ >; const included = rawItems.map((item, index) => { - const productSku = (item['productSku'] as string | undefined)?.trim(); - const productUnitId = - (item['productUnitId'] as string | undefined)?.trim() || - (item['productUnit'] as string | undefined); - const attrs: Record = { position: index + 1, lineNumber: item['lineNumber'] || String(index + 1), description: item['description'], quantity: Number(item['quantity']), + unitOfQuantity: item['unitOfQuantity'], unitPrice: Number(item['unitPrice']), rowTotal: Number(item['rowTotal']), }; if (item['note']) attrs['note'] = item['note']; - const liRels: Record = {}; - if (productSku) { - liRels['product'] = { data: { type: 'products', id: productSku } }; - } - if (productUnitId) { - liRels['productUnit'] = { - data: { type: 'productunits', id: productUnitId }, - }; - } - return { type: 'invoicelineitems', id: `li_${index + 1}`, attributes: attrs, - ...(Object.keys(liRels).length > 0 ? { relationships: liRels } : {}), }; }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index 1f9f8f8a82a1..f493a1c21d48 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -9,6 +9,8 @@ import { ownerDropdown, websiteDropdown, orderInternalStatusDropdown, + orderStatusDropdown, + orderDropdown, paymentTermDropdown, warehouseDropdown, buildCountryDropdown, @@ -68,6 +70,21 @@ export const createOrderAction = createAction({ description: 'Shipping cost calculated from the selected shipping method.', required: false, }), + shippingMethod: Property.ShortText({ + displayName: 'Shipping Method', + description: 'The shipping method selected for the order (e.g. "flat_rate_2").', + required: false, + }), + shippingMethodType: Property.ShortText({ + displayName: 'Shipping Method Type', + description: 'The shipping method type (e.g. "primary").', + required: false, + }), + disablePromotions: Property.Checkbox({ + displayName: 'Disable Promotions', + description: 'Prevent the promotions engine from running for this order.', + required: false, + }), // -- Optional relationships ------------------------------------------------ customerUser: customerUserDropdown(false), @@ -112,6 +129,18 @@ export const createOrderAction = createAction({ description: 'Overrides the Warehouse dropdown.', required: false, }), + parent: orderDropdown, + parentId: Property.ShortText({ + displayName: 'Parent Order: Raw ID', + description: 'Overrides the Parent Order dropdown. Paste an order ID from a previous step.', + required: false, + }), + status: orderStatusDropdown, + statusId: Property.ShortText({ + displayName: 'Status: Raw ID', + description: 'Overrides the Status dropdown. Only applied when "Enable External Status Management" is on.', + required: false, + }), // -- Billing Address ------------------------------------------------------- billingAddressLabel: Property.ShortText({ @@ -245,6 +274,7 @@ export const createOrderAction = createAction({ props: async ({ auth }) => { type JsonApiCollection = { data: { id: string; attributes: Record }[] }; const unitOptions: { label: string; value: string }[] = []; + const warehouseOptions: { label: string; value: string }[] = []; if (auth) { try { @@ -261,6 +291,21 @@ export const createOrderAction = createAction({ }); } } catch { /* leave empty */ } + + try { + const warehousesResp = await oroApiCall({ + method: HttpMethod.GET, + resourceUri: '/warehouses', + auth: auth as OroAuth, + queryParams: { 'page[size]': '100', 'fields[warehouses]': 'id,name' }, + }); + for (const item of (warehousesResp.body as JsonApiCollection).data ?? []) { + warehouseOptions.push({ + label: String(item.attributes['name'] || item.id), + value: item.id, + }); + } + } catch { /* leave empty */ } } return { @@ -268,9 +313,30 @@ export const createOrderAction = createAction({ displayName: 'Line Items', required: true, properties: { + // -- Product relationship -------------------------------------- + productId: Property.ShortText({ + displayName: 'Product: Raw ID', + description: + 'Numeric Oro product ID. Use the top-level "Product Search" field to look up a product.', + required: false, + }), + // -- Optional attributes --------------------------------------- + productName: Property.ShortText({ + displayName: 'Product Name', + description: 'Default name of the ordered product.', + required: false, + }), + freeFormProduct: Property.ShortText({ + displayName: 'Free-Form Product', + description: + 'Product name for free-form (non-catalog) line items.', + required: false, + }), + // -- Required -------------------------------------------------- productSku: Property.ShortText({ - displayName: 'Product SKU or ID', - description: 'Enter the product SKU or numeric ID. Use the top-level "Product (Search)" field to look up products.', + displayName: 'Product SKU', + description: + 'Unique human-readable product identifier. Required.', required: true, }), productUnit: Property.StaticDropdown({ @@ -279,32 +345,71 @@ export const createOrderAction = createAction({ required: true, options: { disabled: false, options: unitOptions }, }), - productUnitId: Property.ShortText({ + productUnitRawId: Property.ShortText({ displayName: 'Product Unit: Raw ID', - description: 'Overrides the Product Unit dropdown (e.g. "piece", "kg").', + description: + 'Overrides the Product Unit dropdown (e.g. "piece", "kg").', required: false, }), quantity: Property.Number({ displayName: 'Quantity', + description: 'Quantity of the product ordered.', required: true, }), value: Property.Number({ displayName: 'Unit Price', - description: 'Price per unit used in this order.', + description: 'Price per unit used in this order. Required.', required: true, }), currency: Property.ShortText({ displayName: 'Currency', - description: 'ISO-4217 code. Defaults to order currency if empty.', + description: + 'ISO-4217 code. Defaults to order-level currency if empty.', required: false, }), - comment: Property.ShortText({ + comment: Property.LongText({ displayName: 'Comment', + description: 'Comments to the line item.', required: false, }), shipBy: Property.ShortText({ displayName: 'Ship By Date', - description: 'YYYY-MM-DD. Latest acceptable ship date for this line.', + description: + 'Latest acceptable ship date for this line (YYYY-MM-DD).', + required: false, + }), + priceType: Property.Number({ + displayName: 'Price Type', + description: + 'Type of the product price (e.g. 10 = unit price).', + required: false, + defaultValue: 10, + }), + shippingEstimateAmount: Property.Number({ + displayName: 'Shipping Estimate Amount', + description: 'Calculated shipping cost for this line item.', + required: false, + }), + shippingMethod: Property.ShortText({ + displayName: 'Shipping Method', + description: 'Shipping method assigned to this line item.', + required: false, + }), + shippingMethodType: Property.ShortText({ + displayName: 'Shipping Method Type', + description: 'Shipping method type assigned to this line item.', + required: false, + }), + // -- Warehouse relationship ------------------------------------ + warehouse: Property.StaticDropdown({ + displayName: 'Warehouse', + description: 'Warehouse this line item ships from.', + required: false, + options: { disabled: false, options: warehouseOptions }, + }), + warehouseId: Property.ShortText({ + displayName: 'Warehouse: Raw ID', + description: 'Overrides the Warehouse dropdown.', required: false, }), }, @@ -400,7 +505,7 @@ export const createOrderAction = createAction({ }; } - // -- Shipping address --------------────────────────────────────────────── + // -- Shipping address ---------------------------------------------------- const shippingAdded = buildAddress('shipping_address', { label: p.shippingAddressLabel, firstName: p.shippingAddressFirstName, @@ -421,7 +526,7 @@ export const createOrderAction = createAction({ }; } - // ── Line items ────────────────────────────────────────────────────────── + // -- Line items ---------------------------------------------------------- // DynamicProperties wraps the array in an object keyed by "lineItems" const dynamicValue = (p.lineItems ?? {}) as Record; const rawItems = (dynamicValue['lineItems'] ?? []) as Array>; @@ -429,28 +534,39 @@ export const createOrderAction = createAction({ const lineItemsRelData = rawItems.map((item, index) => { const lid = `li_${index + 1}`; - // productSku holds either a numeric Oro ID or a SKU string - const productSku = (item['productSku'] as string | undefined)?.trim(); - const productUnitId = (item['productUnitId'] as string | undefined)?.trim() - || (item['productUnit'] as string | undefined) + const productUnitId = (item['productUnitRawId'] as string | undefined)?.trim() + || (item['productUnit'] as string | undefined) || 'piece'; const liAttributes: Record = { + productSku: String(item['productSku'] ?? ''), quantity: Number(item['quantity']), value: Number(item['value']), currency: item['currency'] || p.currency || 'USD', - priceType: 10, - fromExternalSource: false, - checksum: String(index), + priceType: item['priceType'] != null ? Number(item['priceType']) : 10, + fromExternalSource: true, }; - if (item['comment']) liAttributes['comment'] = item['comment']; - if (item['shipBy']) liAttributes['shipBy'] = item['shipBy']; + if (item['productName']) liAttributes['productName'] = item['productName']; + if (item['freeFormProduct']) liAttributes['freeFormProduct'] = item['freeFormProduct']; + if (item['comment']) liAttributes['comment'] = item['comment']; + if (item['shipBy']) liAttributes['shipBy'] = item['shipBy']; + if (item['shippingEstimateAmount'] != null) liAttributes['shippingEstimateAmount'] = Number(item['shippingEstimateAmount']); + if (item['shippingMethod']) liAttributes['shippingMethod'] = item['shippingMethod']; + if (item['shippingMethodType']) liAttributes['shippingMethodType'] = item['shippingMethodType']; const liRelationships: Record = { productUnit: { data: { type: 'productunits', id: String(productUnitId) } }, }; - if (productSku) { - liRelationships['product'] = { data: { type: 'products', id: productSku } }; + + const productId = (item['productId'] as string | undefined)?.trim(); + if (productId) { + liRelationships['product'] = { data: { type: 'products', id: productId } }; + } + + const liWarehouseId = (item['warehouseId'] as string | undefined)?.trim() + || (item['warehouse'] as string | undefined)?.trim(); + if (liWarehouseId) { + liRelationships['warehouse'] = { data: { type: 'warehouses', id: liWarehouseId } }; } included.push({ @@ -464,7 +580,7 @@ export const createOrderAction = createAction({ }); relationships['lineItems'] = { data: lineItemsRelData }; - // ── Order attributes ──────────────────────────────────────────────────── + // -- Order attributes ---------------------------------------------------- const attributes: Record = { external: false }; if (p.currency) attributes['currency'] = p.currency; if (p.identifier) attributes['identifier'] = p.identifier; @@ -473,8 +589,11 @@ export const createOrderAction = createAction({ if (p.shipUntil) attributes['shipUntil'] = p.shipUntil; if (p.overriddenShippingCostAmount != null) attributes['overriddenShippingCostAmount'] = p.overriddenShippingCostAmount; if (p.estimatedShippingCostAmount != null) attributes['estimatedShippingCostAmount'] = p.estimatedShippingCostAmount; + if (p.shippingMethod) attributes['shippingMethod'] = p.shippingMethod; + if (p.shippingMethodType) attributes['shippingMethodType'] = p.shippingMethodType; + if (p.disablePromotions) attributes['disablePromotions'] = p.disablePromotions; - // ── Order relationships — each resolved via raw ID → dropdown fallback ── + // -- Order relationships — each resolved via raw ID → dropdown fallback -- const customerId = resolve(p.customerId, p.customer); if (customerId) { relationships['customer'] = { data: { type: 'customers', id: customerId } }; @@ -515,7 +634,17 @@ export const createOrderAction = createAction({ relationships['warehouse'] = { data: { type: 'warehouses', id: warehouseId } }; } - // ── POST /orders ──────────────────────────────────────────────────────── + const parentId = resolve(p.parentId, p.parent); + if (parentId) { + relationships['parent'] = { data: { type: 'orders', id: parentId } }; + } + + const statusId = resolve(p.statusId, p.status); + if (statusId) { + relationships['status'] = { data: { type: 'orderstatuses', id: statusId } }; + } + + // -- POST /orders --------------------------------------------------------- const response = await oroApiCall({ method: HttpMethod.POST, resourceUri: '/orders', diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index 5c8523efe8f3..0efb9a6c19eb 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -10,6 +10,8 @@ import { type OroAuth, type OroAuthResponseType, type OroApiCallParams, + OroJsonApiItem, + OroJsonApiCollection, } from './types'; let cachedToken: string | null = null; @@ -70,3 +72,18 @@ export async function oroApiCall({ ); } } + +export async function fetchCollection( + auth: OroAuth, + resourceUri: string, + queryParams?: Record +): Promise { + const response = await oroApiCall({ + method: HttpMethod.GET, + resourceUri, + auth, + queryParams: { 'page[size]': '50', ...queryParams }, + }); + + return (response.body as OroJsonApiCollection).data ?? []; +} diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index 34d65eba75f1..0ba0b9be9204 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -1,20 +1,8 @@ import { Property } from '@activepieces/pieces-framework'; import { HttpMethod } from '@activepieces/pieces-common'; import { oroAuth } from './auth'; -import { oroApiCall } from './client'; -import { OroAuth } from './types'; - -// --- Internal types ----------------------------------------------------------- - -interface OroJsonApiItem { - id: string; - type: string; - attributes: Record; -} - -interface OroJsonApiCollection { - data: OroJsonApiItem[]; -} +import { oroApiCall, fetchCollection } from './client'; +import { OroAuth, OroJsonApiItem } from './types'; // --- Helpers ------------------------------------------------------------------ @@ -30,21 +18,6 @@ const FAILED = { options: [], }; -async function fetchCollection( - auth: OroAuth, - resourceUri: string, - queryParams?: Record -): Promise { - const response = await oroApiCall({ - method: HttpMethod.GET, - resourceUri, - auth, - queryParams: { 'page[size]': '50', ...queryParams }, - }); - - return (response.body as OroJsonApiCollection).data ?? []; -} - // --- Customers ---------------------------------------------------------------- // Supports filter[searchText] — search across name and other indexed fields. @@ -96,8 +69,7 @@ export const customerRequiredDropdown = buildCustomerOptions( ); // --- Customer Users ----------------------------------------------------------- -// Supports filter[searchText] — searches email, firstName, lastName, etc. -// Also pre-filters by customer relationship when customer is selected. +// Pre-filters by customer relationship when customer is selected. export const customerUserDropdown = (required = false) => Property.Dropdown({ @@ -152,7 +124,6 @@ export const customerUserDropdown = (required = false) => }); // --- Organizations ------------------------------------------------------------ -// Supports filter[searchText]. export const organizationDropdown = Property.Dropdown({ auth: oroAuth, @@ -189,7 +160,6 @@ export const organizationDropdown = Property.Dropdown({ }); // --- Owner (admin users) ------------------------------------------------------ -// Supports filter[searchText] — searches username, email, firstName, lastName. export const ownerDropdown = Property.Dropdown({ auth: oroAuth, @@ -228,7 +198,6 @@ export const ownerDropdown = Property.Dropdown({ }); // --- Websites ----------------------------------------------------------------- -// Supports filter[searchText]. Small set in practice — still search-gated. export const websiteDropdown = Property.Dropdown({ auth: oroAuth, @@ -259,7 +228,7 @@ export const websiteDropdown = Property.Dropdown({ }, }); -// --- Internal Statuses (Invoice) ---------------------------------------------- +// --- Invoice Internal Statuses ---------------------------------------------- // Small enum — loaded once, no search needed. export const invoiceInternalStatusDropdown = Property.Dropdown({ @@ -315,8 +284,7 @@ export const orderInternalStatusDropdown = Property.Dropdown({ }, }); -// --- Products ---────────────────────────────────────────────────────────────── -// Supports filter[searchText] — searches SKU, names, descriptions, etc. +// --- Products ----------------------------------------------------------------- export const productDropdown = Property.Dropdown({ auth: oroAuth, @@ -329,7 +297,9 @@ export const productDropdown = Property.Dropdown({ if (!auth) return NOT_CONNECTED; try { const params: Record = { - 'fields[products]': 'sku,status,names', + 'fields[products]': 'id,sku,status,names', + 'include': 'names', + 'fields[productnames]': 'id,string,localization', }; if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}" and productStatus = "enabled"`; @@ -337,12 +307,42 @@ export const productDropdown = Property.Dropdown({ params['filter[status]'] = 'enabled'; } - const items = await fetchCollection(auth as OroAuth, '/products', params); + const response = await oroApiCall({ + method: HttpMethod.GET, + resourceUri: '/products', + auth: auth as OroAuth, + queryParams: { 'page[size]': '50', ...params }, + }); + + const body = response.body as { + data: OroJsonApiItem[]; + included?: { type: string; id: string; attributes: Record; relationships: Record }[]; + }; + + // Build a map: productId → default name (localization.data === null) + const nameMap: Record = {}; + for (const inc of body.included ?? []) { + if (inc.type === 'productnames') { + const localizationRel = inc.relationships['localization'] as { data: { type: string; id: string } | null } | undefined; + if (localizationRel?.data === null) { + const productRel = inc.relationships['product'] as { data: { type: string; id: string } | null } | undefined; + const productId = productRel?.data?.id; + if (productId) { + nameMap[productId] = String(inc.attributes['string'] ?? ''); + } + } + } + } + return { - options: items.map((item) => ({ - label: String(item.attributes['sku'] ?? item.id), - value: item.id, - })), + options: (body.data ?? []).map((item) => { + const sku = String(item.attributes['sku'] ?? ''); + const name = nameMap[item.id] ?? ''; + const label = name + ? `${item.id}: ${sku} - ${name}` + : `${item.id}: ${sku}`; + return { label, value: item.id }; + }), }; } catch { return FAILED; @@ -350,8 +350,7 @@ export const productDropdown = Property.Dropdown({ }, }); -// ─── Payment Terms ──────────────────────────────────────────────────────────── -// Supports filter[searchText]. +// --- Payment Terms ------------------------------------------------------------ export const paymentTermDropdown = Property.Dropdown({ auth: oroAuth, @@ -387,7 +386,7 @@ export const paymentTermDropdown = Property.Dropdown({ }, }); -// ─── Warehouses ─────────────────────────────────────────────────────────────── +// --- Warehouses --------------------------------------------------------------- // Supports filter[searchText]. export const warehouseDropdown = Property.Dropdown({ @@ -423,7 +422,7 @@ export const warehouseDropdown = Property.Dropdown({ }, }); -// ─── Countries ──────────────────────────────────────────────────────────────── +// --- Countries ---------------------------------------------------------------- // Static dictionary — load full list once per connection (cached by auth refresher). // Uses filter[name] for client-side filtering via the UI's built-in search box. // Page size 300 to capture all ~250 ISO countries in one request. @@ -441,9 +440,6 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') const params: Record = { 'fields[countries]': 'id,name' }; - // if (searchValue && searchValue.trim().length > 0) { - // params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; - // } const items = await fetchCollection( auth as OroAuth, @@ -452,6 +448,15 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') ); return { options: items + .filter(function (item) { + if (!searchValue || searchValue?.length == 0) { + return true; + } + + const searchBase = String(item.attributes['name'] ?? item.id); + + return searchBase.toLowerCase().includes(searchValue.toLowerCase()); + }) .map((item) => ({ label: String(item.attributes['name'] ?? item.id), value: item.id, // ISO2 code e.g. "US" @@ -464,7 +469,7 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') }, }); -// ─── Regions ────────────────────────────────────────────────────────────────── +// --- Regions ------------------------------------------------------------------ // Static dictionary per country — load all regions for the selected country once. // Cached by auth + countryRefresher. Uses filter[country] to scope results. @@ -495,14 +500,19 @@ export const buildRegionDropdown = ( }; try { const params: Record = {'filter[country]': countryId}; - // if (searchValue && searchValue.trim().length > 0) { - // params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; - // params['filter[name]'] = searchValue.trim(); - // } const items = await fetchCollection(auth, '/regions', params); return { options: items + .filter(function (item) { + if (!searchValue || searchValue?.length == 0) { + return true; + } + + const searchBase = String(item.attributes['name'] ?? item.id); + + return searchBase.toLowerCase().includes(searchValue.toLowerCase()); + }) .map((item) => ({ label: String(item.attributes['name'] ?? item.id), value: item.id, // ISO 3166-2 code e.g. "US-NY" @@ -515,7 +525,67 @@ export const buildRegionDropdown = ( }, }); -// ─── Product Units ──────────────────────────────────────────────────────────── +// --- Order Statuses (external) ------------------------------------------------ +// Small enum managed by an external system. Loaded once, no search needed. + +export const orderStatusDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Status', + description: 'Order status managed by an external system (only relevant when "Enable External Status Management" is on).', + required: false, + refreshers: [], + options: async ({ auth }) => { + if (!auth) return NOT_CONNECTED; + try { + const items = await fetchCollection(auth as OroAuth, '/orderstatuses'); + return { + options: items.map((item) => ({ + label: String(item.attributes['name'] ?? item.id), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Parent Order ------------------------------------------------------------- + +export const orderDropdown = Property.Dropdown({ + auth: oroAuth, + displayName: 'Parent Order', + description: 'Search orders to use as the parent order.', + required: false, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) return NOT_CONNECTED; + try { + const params: Record = { + 'fields[orders]': 'id,identifier,poNumber', + }; + if (searchValue && searchValue.trim().length > 0) { + params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; + } + const items = await fetchCollection(auth as OroAuth, '/orders', params); + return { + options: items.map((item) => ({ + label: String( + item.attributes['identifier'] ?? + item.attributes['poNumber'] ?? + item.id + ), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Product Units ------------------------------------------------------------ // Small static list (each, set, kg, …) — load all once per connection. export const productUnitDropdown = Property.Dropdown({ diff --git a/packages/pieces/community/orocommerce/src/lib/common/types.ts b/packages/pieces/community/orocommerce/src/lib/common/types.ts index 960fca0ef2f9..b6dce5570116 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/types.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -23,3 +23,13 @@ export type OroApiCallParams = { queryParams?: QueryParams; body?: HttpMessageBody; }; + +export interface OroJsonApiItem { + id: string; + type: string; + attributes: Record; +} + +export interface OroJsonApiCollection { + data: OroJsonApiItem[]; +} From d15d6247b09eb971019b4fdd04993dcbd1a57023 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 23 Feb 2026 19:11:21 +0100 Subject: [PATCH 05/50] feat(orocommerce): improved order and invoice actions --- .../src/lib/actions/create-invoice.ts | 4 ++-- .../src/lib/actions/create-order.ts | 4 ++-- .../orocommerce/src/lib/common/props.ts | 24 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index aef4138377ba..e6ae137f37d8 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -7,7 +7,7 @@ import { customerUserDropdown, invoiceInternalStatusDropdown, organizationDropdown, - ownerDropdown, + userDropdown, websiteDropdown, } from '../common'; @@ -120,7 +120,7 @@ export const createInvoiceAction = createAction({ description: 'Overrides the Organization dropdown.', required: false, }), - owner: ownerDropdown, + owner: userDropdown, ownerId: Property.ShortText({ displayName: 'Owner: Raw ID', description: 'Overrides the Owner dropdown.', diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index f493a1c21d48..d89cd77647b1 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -6,7 +6,7 @@ import { customerRequiredDropdown, customerUserDropdown, organizationDropdown, - ownerDropdown, + userDropdown, websiteDropdown, orderInternalStatusDropdown, orderStatusDropdown, @@ -99,7 +99,7 @@ export const createOrderAction = createAction({ description: 'Overrides the Organization dropdown.', required: false, }), - owner: ownerDropdown, + owner: userDropdown, ownerId: Property.ShortText({ displayName: 'Owner: Raw ID', description: 'Overrides the Owner dropdown.', diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index 0ba0b9be9204..a2f6a85572d6 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -161,11 +161,10 @@ export const organizationDropdown = Property.Dropdown({ // --- Owner (admin users) ------------------------------------------------------ -export const ownerDropdown = Property.Dropdown({ +export const userDropdown = Property.Dropdown({ auth: oroAuth, - displayName: 'Owner', - description: - 'Search user by name, username or email.', + displayName: 'User', + description: 'Search user by name, username or email.', required: false, refreshers: [], refreshOnSearch: true, @@ -173,7 +172,7 @@ export const ownerDropdown = Property.Dropdown({ if (!auth) return NOT_CONNECTED; try { const params: Record = { - 'fields[users]': 'id,firstName,lastName,username' + 'fields[users]': 'id,firstName,lastName,username', }; if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; @@ -182,12 +181,12 @@ export const ownerDropdown = Property.Dropdown({ return { options: items.map((item) => { const firstName = String(item.attributes['firstName'] ?? ''); - const lastName = String(item.attributes['lastName'] ?? ''); - const username = String(item.attributes['username'] ?? ''); - const label = - [firstName, lastName].filter(Boolean).join(' ') || - username || - item.id; + const lastName = String(item.attributes['lastName'] ?? ''); + const username = String(item.attributes['username'] ?? ''); + const fullName = [firstName, lastName].filter(Boolean).join(' '); + const label = username + ? fullName ? `${username}: ${fullName}` : username + : fullName || item.id; return { label, value: item.id }; }), }; @@ -438,7 +437,8 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') if (!auth) return NOT_CONNECTED; try { const params: Record = { - 'fields[countries]': 'id,name' + 'fields[countries]': 'id,name', + 'page[size]': '300', }; const items = await fetchCollection( From 04ef4a396f0ff7519696352df249125b77fe2678 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 23 Feb 2026 19:16:38 +0100 Subject: [PATCH 06/50] feat(orocommerce): added API call action --- .../pieces/community/orocommerce/src/index.ts | 4 +- .../orocommerce/src/lib/actions/api-call.ts | 76 +++++++++++++++++++ .../orocommerce/src/lib/actions/index.ts | 2 +- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/api-call.ts diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index cb424e61eda5..359e72c7553e 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -3,7 +3,7 @@ import { PieceCategory } from '@activepieces/shared'; import { oroAuth } from './lib/common'; import { newOrder, removedOrder, updatedOrder } from './lib/triggers/order'; import { newInvoice, removedInvoice, updatedInvoice } from './lib/triggers/invoice'; -import { createInvoiceAction, createOrderAction } from './lib/actions'; +import { createInvoiceAction, createOrderAction, apiCallAction } from './lib/actions'; export const orocommerce = createPiece({ displayName: 'OroCommerce', @@ -14,7 +14,7 @@ export const orocommerce = createPiece({ categories: [PieceCategory.COMMERCE], description: 'B2B digital commerce solution', authors: ['Oro Inc.'], - actions: [createInvoiceAction, createOrderAction], + actions: [createInvoiceAction, createOrderAction, apiCallAction], triggers: [ newOrder, removedOrder, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts new file mode 100644 index 000000000000..d632acbf4895 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -0,0 +1,76 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { oroAuth, oroApiCall } from '../common'; + +export const apiCallAction = createAction({ + auth: oroAuth, + name: 'api_call', + displayName: 'API Call', + description: 'Make a direct authenticated call to the OroCommerce JSON:API.', + props: { + method: Property.StaticDropdown({ + displayName: 'Method', + required: true, + defaultValue: 'GET', + options: { + options: [ + { label: 'GET', value: 'GET' }, + { label: 'POST', value: 'POST' }, + { label: 'PATCH', value: 'PATCH' }, + { label: 'DELETE', value: 'DELETE' }, + ], + }, + }), + resourceUri: Property.ShortText({ + displayName: 'Resource URI', + description: + 'Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open', + required: true, + }), + queryParams: Property.Json({ + displayName: 'Query Parameters', + description: 'Additional query parameters as a JSON object, e.g. {"filter[status]":"open","page[size]":"20"}', + required: false, + defaultValue: {}, + }), + body: Property.Json({ + displayName: 'Request Body', + description: 'Request body as a JSON object (used for POST / PATCH).', + required: false, + defaultValue: {}, + }), + }, + + async run(context) { + const { method, resourceUri, queryParams, body } = context.propsValue; + + // Build query params — strip empty/null values + const qp: Record = {}; + for (const [k, v] of Object.entries( + (queryParams as Record) ?? {} + )) { + if (v != null && String(v).trim() !== '') { + qp[k] = String(v); + } + } + + const hasBody = + body != null && + typeof body === 'object' && + Object.keys(body as object).length > 0; + + const response = await oroApiCall({ + method: method as HttpMethod, + resourceUri, + auth: context.auth, + queryParams: Object.keys(qp).length > 0 ? qp : undefined, + body: hasBody ? body : undefined, + }); + + return { + status: response.status, + body: response.body, + }; + }, +}); + diff --git a/packages/pieces/community/orocommerce/src/lib/actions/index.ts b/packages/pieces/community/orocommerce/src/lib/actions/index.ts index 5d07c848e973..482ad9a7703c 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/index.ts @@ -1,3 +1,3 @@ export { createInvoiceAction } from './create-invoice'; export { createOrderAction } from './create-order'; - +export { apiCallAction } from './api-call'; From d45bf621b754f98b5267864e6db8b8f6c2b8794e Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Tue, 24 Feb 2026 16:30:50 +0100 Subject: [PATCH 07/50] feat(orocommerce): added headers to api call, improved JSON:API serialize/deserialize actions --- .../pieces/community/orocommerce/src/index.ts | 22 +- .../orocommerce/src/lib/actions/api-call.ts | 23 +- .../orocommerce/src/lib/actions/index.ts | 2 + .../src/lib/actions/serialize-jsonapi.ts | 102 +++++ .../src/lib/actions/unserialize-jsonapi.ts | 29 ++ .../orocommerce/src/lib/common/client.ts | 2 + .../src/lib/common/jsonapi/deserialize.ts | 73 ++++ .../src/lib/common/jsonapi/index.ts | 6 + .../src/lib/common/jsonapi/serialize.ts | 211 ++++++++++ .../src/lib/common/jsonapi/types.ts | 71 ++++ .../orocommerce/src/lib/common/types.ts | 1 + .../orocommerce/src/lib/examples/invoice.ts | 364 +++++------------- 12 files changed, 628 insertions(+), 278 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index 359e72c7553e..ac49df717ce8 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -2,8 +2,18 @@ import { createPiece } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/shared'; import { oroAuth } from './lib/common'; import { newOrder, removedOrder, updatedOrder } from './lib/triggers/order'; -import { newInvoice, removedInvoice, updatedInvoice } from './lib/triggers/invoice'; -import { createInvoiceAction, createOrderAction, apiCallAction } from './lib/actions'; +import { + newInvoice, + removedInvoice, + updatedInvoice, +} from './lib/triggers/invoice'; +import { + createInvoiceAction, + createOrderAction, + apiCallAction, + serializeJsonApiAction, + unserializeJsonApiAction, +} from './lib/actions'; export const orocommerce = createPiece({ displayName: 'OroCommerce', @@ -14,7 +24,13 @@ export const orocommerce = createPiece({ categories: [PieceCategory.COMMERCE], description: 'B2B digital commerce solution', authors: ['Oro Inc.'], - actions: [createInvoiceAction, createOrderAction, apiCallAction], + actions: [ + createInvoiceAction, + createOrderAction, + apiCallAction, + serializeJsonApiAction, + unserializeJsonApiAction, + ], triggers: [ newOrder, removedOrder, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts index d632acbf4895..518ef7143fbe 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -27,6 +27,15 @@ export const apiCallAction = createAction({ 'Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open', required: true, }), + headers: Property.Json({ + displayName: 'Headers', + description: 'Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.', + required: false, + defaultValue: { + 'Accept': 'application/vnd.api+json', + 'X-Include': 'noHateoas;totalCount', + }, + }), queryParams: Property.Json({ displayName: 'Query Parameters', description: 'Additional query parameters as a JSON object, e.g. {"filter[status]":"open","page[size]":"20"}', @@ -42,7 +51,17 @@ export const apiCallAction = createAction({ }, async run(context) { - const { method, resourceUri, queryParams, body } = context.propsValue; + const { method, resourceUri, headers, queryParams, body } = context.propsValue; + + // Build headers — strip empty/null values + const hdrs: Record = {}; + for (const [k, v] of Object.entries( + (headers as Record) ?? {} + )) { + if (v != null && String(v).trim() !== '') { + hdrs[k] = String(v); + } + } // Build query params — strip empty/null values const qp: Record = {}; @@ -63,6 +82,7 @@ export const apiCallAction = createAction({ method: method as HttpMethod, resourceUri, auth: context.auth, + headers: Object.keys(hdrs).length > 0 ? hdrs : undefined, queryParams: Object.keys(qp).length > 0 ? qp : undefined, body: hasBody ? body : undefined, }); @@ -73,4 +93,3 @@ export const apiCallAction = createAction({ }; }, }); - diff --git a/packages/pieces/community/orocommerce/src/lib/actions/index.ts b/packages/pieces/community/orocommerce/src/lib/actions/index.ts index 482ad9a7703c..951167afc45b 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/index.ts @@ -1,3 +1,5 @@ export { createInvoiceAction } from './create-invoice'; export { createOrderAction } from './create-order'; export { apiCallAction } from './api-call'; +export { serializeJsonApiAction } from './serialize-jsonapi'; +export { unserializeJsonApiAction } from './unserialize-jsonapi'; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts b/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts new file mode 100644 index 000000000000..bf58877097b2 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts @@ -0,0 +1,102 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { serialize, type FlatResource, type Linkage, type JsonApiResource } from '../common/jsonapi'; + +export const serializeJsonApiAction = createAction({ + name: 'serialize_jsonapi', + displayName: 'Serialize JSON:API Request', + description: + 'Converts a plain object into a JSON:API-compliant request body. ' + + 'Relationship fields are detected automatically - any array or object value is treated as a ' + + 'relationship, only scalars become attributes. ' + + 'The result can be passed directly as the Request Body of the API Call action.', + auth: undefined, + props: { + resourceType: Property.ShortText({ + displayName: 'Resource Type', + description: + 'The JSON:API resource type (e.g. orders, invoices, products). ' + + 'The _type resolved as resource type automatically.', + required: false, + }), + resourceId: Property.ShortText({ + displayName: 'Resource ID', + description: + 'ID of the resource - leave empty when creating a new record (POST) ' + + 'or when the input includes id field (the id field is picked up automatically).', + required: false, + }), + attributes: Property.Json({ + displayName: 'Attributes', + description: + 'The flat object to serialize. Pass the full JSON ' + + 'document here - arrays and objects are automatically placed into ' + + '"relationships"; only scalar values become attributes. ' + + 'For manual construction use plain key/value pairs, e.g. ' + + '{"currency":"USD","poNumber":"PO-001"}.', + required: true, + defaultValue: {}, + }), + relationships: Property.Json({ + displayName: 'Relationships (override)', + description: + 'Optional explicit relationship map that takes priority over any ' + + 'relationships detected inside Attributes. ' + + 'Each key is a relationship name; the value is a single linkage object ' + + '{"type":"customers","id":"42"}, an array of linkage objects, or null. ' + + 'Example: {"customer":{"type":"customers","id":"42"},' + + '"lineItems":[{"type":"orderlineitems","id":"1"}]}', + required: false, + defaultValue: {}, + }), + included: Property.Json({ + displayName: 'Included', + description: + 'Optional array of full JSON:API resource objects to embed in the ' + + '"included" section of the request. If the input to Attributes already ' + + 'contains an "included" array (e.g. the raw API Call response body) it ' + + 'will be forwarded automatically - this field only needs to be set when ' + + 'adding or overriding included resources manually. Each entry must be a ' + + 'valid JSON:API resource with "type", "id", and "attributes" fields.', + required: false, + defaultValue: [], + }), + }, + + async run(context) { + const { resourceType, resourceId, attributes, relationships, included } = context.propsValue; + + const flat = (attributes as FlatResource) ?? {}; + + // If the caller passed a full JSON:API document that contains an `included` + // array at the top level, extract and forward it automatically. + const docIncluded = Array.isArray((flat as Record)['included']) + ? ((flat as Record)['included'] as unknown as JsonApiResource[]) + : []; + const explicitIncluded = Array.isArray(included) + ? (included as unknown as JsonApiResource[]) + : []; + const mergedIncluded = explicitIncluded.length > 0 ? explicitIncluded : docIncluded; + + // Fall back to _type embedded in the flat object (Unserialize action output) + const resolvedType = + (resourceType && resourceType.trim() !== '' ? resourceType.trim() : undefined) ?? + (typeof flat['_type'] === 'string' && flat['_type'].trim() !== '' + ? (flat['_type'] as string).trim() + : undefined); + + if (!resolvedType) { + throw new Error( + 'Resource Type is required. Either fill in the "Resource Type" field or ' + + 'pass the output of the Unserialize action (which carries a _type field).' + ); + } + + return serialize({ + type: resolvedType, + id: resourceId ?? undefined, + data: flat, + relationships: (relationships as Record) ?? {}, + included: mergedIncluded, + }); + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts b/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts new file mode 100644 index 000000000000..7a9d4adf4e5d --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts @@ -0,0 +1,29 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { deserialize, type JsonApiDocument } from '../common/jsonapi'; + +export const unserializeJsonApiAction = createAction({ + name: 'unserialize_jsonapi', + displayName: 'Unserialize JSON:API Response', + description: + 'Flattens a JSON:API response body into a plain object (or array of objects). ' + + 'Included relationships are resolved and inlined. Unresolved relationship ' + + 'references are kept as { _type, id } stubs so the document can be ' + + 're-serialized back to a valid JSON:API request without data loss. ' + + 'Pass the "body" output of the API Call action as the Response input.', + auth: undefined, + props: { + response: Property.Json({ + displayName: 'JSON:API Response', + description: + 'The raw JSON:API response body - the "body" field from the API Call action output. ' + + 'Must contain a top-level "data" key (single resource or collection).', + required: true, + defaultValue: {}, + }), + }, + + async run(context) { + const doc = context.propsValue.response as JsonApiDocument; + return deserialize(doc); + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index 0efb9a6c19eb..a4effb16a819 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -47,6 +47,7 @@ export async function oroApiCall({ auth, queryParams, body, + headers: extraHeaders, }: OroApiCallParams): Promise> { try { return await httpClient.sendRequest({ @@ -54,6 +55,7 @@ export async function oroApiCall({ url: `${auth.props.serverUrl}/${auth.props.adminPrefix}/api${resourceUri}`, headers: { 'Content-Type': 'application/vnd.api+json', + ...extraHeaders, }, authentication: { type: AuthenticationType.BEARER_TOKEN, diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts new file mode 100644 index 000000000000..817402b36134 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts @@ -0,0 +1,73 @@ +import type { JsonApiDocument, JsonApiResource, Linkage, FlatResource, DeserializeResult } from './types'; + +/** + * Sentinel value written by the deserializer for a null to-one relationship. + * Carrying `_type: null` lets the serializer distinguish it from a plain + * null attribute and round-trip it back to `{ data: null }`. + */ +export const NULL_RELATIONSHIP: FlatResource = Object.freeze({ _type: null, id: null }); + +// -- internal helpers ------------------------------------------------------- + +function buildIndex(included: JsonApiResource[]): Map { + const m = new Map(); + for (const r of included) m.set(`${r.type}:${r.id}`, r); + return m; +} + +function resolveRef( + ref: Linkage, + index: Map, + visited: Set, +): FlatResource { + const key = `${ref.type}:${ref.id}`; + // cycle guard or not in included -> minimal restorable stub + if (visited.has(key)) return { _type: ref.type, id: ref.id }; + const resource = index.get(key); + if (resource) return flattenResource(resource, index, new Set(visited)); + return { _type: ref.type, id: ref.id }; +} + +function flattenResource( + r: JsonApiResource, + index: Map, + visited: Set, +): FlatResource { + visited.add(`${r.type}:${r.id}`); + const out: FlatResource = { ...r.attributes, _type: r.type, id: r.id }; + for (const [name, rel] of Object.entries(r.relationships ?? {})) { + const lnk = rel.data; + if (lnk === null || lnk === undefined) { + // null to-one relationship — use sentinel so serializer can restore it + out[name] = NULL_RELATIONSHIP; + continue; + } + if (Array.isArray(lnk)) { + // empty array stays as empty array; elements are resolved individually + out[name] = lnk.map((ref) => resolveRef(ref, index, new Set(visited))); + continue; + } + out[name] = resolveRef(lnk, index, new Set(visited)); + } + return out; +} + +// -- public API ------------------------------------------------------------- + +/** + * Deserialize a JSON:API document into a plain flat object or array. + * + * - Resources in `included` are resolved and inlined. + * - Relationship references **not** found in `included` become `{ _type, id }` + * stubs instead of `null`, preserving all information needed to re-serialize. + * - Null to-one relationships become `{ _type: null, id: null }` sentinels. + * - Circular references (e.g. order -> lineItem -> order) are broken safely. + */ +export function deserialize(doc: JsonApiDocument): DeserializeResult | unknown { + if (!doc?.data) return doc; + const index = buildIndex(doc.included ?? []); + if (Array.isArray(doc.data)) { + return doc.data.map((r) => flattenResource(r, index, new Set())); + } + return flattenResource(doc.data, index, new Set()); +} diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts new file mode 100644 index 000000000000..90bf17f6bf00 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts @@ -0,0 +1,6 @@ +/** + * This library is generated by Claude Sonnet 4.6 + */ +export * from './types'; +export * from './deserialize'; +export * from './serialize'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts new file mode 100644 index 000000000000..0ea5dc8eac40 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts @@ -0,0 +1,211 @@ +import type { FlatResource, JsonApiResource, Linkage, SerializeOptions, SerializeResult } from './types'; + +// -- internal helpers ------------------------------------------------------- + +/** + * Returns true when a value is a plain object that carries a non-null `_type` + * marker injected by `deserialize()` — i.e. a resolved or stub relationship. + */ +function hasTypeMarker(v: unknown): v is FlatResource { + return ( + v !== null && + typeof v === 'object' && + !Array.isArray(v) && + typeof (v as FlatResource)['_type'] === 'string' + ); +} + +/** + * Returns true when a value is the null-relationship sentinel emitted by + * `deserialize()` for JSON:API `{ "data": null }` relationships. + */ +function isNullRelationship(v: unknown): boolean { + return ( + v !== null && + typeof v === 'object' && + !Array.isArray(v) && + (v as FlatResource)['_type'] === null + ); +} + +/** Convert a `_type`-marked flat object back to a JSON:API linkage `{ type, id }`. */ +function toLinkage(obj: FlatResource): Linkage { + return { type: obj['_type'] as string, id: String(obj['id'] ?? '') }; +} + +/** + * Returns true when a `_type`-marked object is a *full* inlined resource + * (has attributes beyond `_type` and `id`) rather than a bare stub. + * + * A stub produced by `deserialize()` for a relationship that was NOT included + * looks like `{ _type: "customers", id: "5" }` — exactly two keys. + * A full inlined resource has additional attribute / relationship keys. + */ +function isFullResource(v: FlatResource): boolean { + return Object.keys(v).some((k) => k !== '_type' && k !== 'id'); +} + +/** + * Convert a full inlined flat resource into a `JsonApiResource` wire object, + * recursing so that nested inlined objects are also hoisted into `collected`. + * + * De-duplicates by `type + id`: if the same resource has already been added to + * `collected` it is skipped (prevents duplicates when billingAddress and + * shippingAddress point to the same record). + */ +function hoistResource(obj: FlatResource, collected: Map): Linkage { + const linkage = toLinkage(obj); + const key = `${linkage.type}::${linkage.id}`; + + if (!collected.has(key)) { + // Reserve the slot immediately to break any circular references + const resource: JsonApiResource = { type: linkage.type, id: linkage.id }; + collected.set(key, resource); + + const attrs: Record = {}; + const rels: Record = {}; + + for (const [k, v] of Object.entries(obj)) { + if (k === '_type' || k === 'id') continue; + + if (isNullRelationship(v)) { + rels[k] = { data: null }; + } else if (Array.isArray(v)) { + rels[k] = { + data: v.map((item) => + hasTypeMarker(item) + ? isFullResource(item) + ? hoistResource(item, collected) + : toLinkage(item) + : (item as Linkage) + ), + }; + } else if (hasTypeMarker(v)) { + rels[k] = { + data: isFullResource(v) ? hoistResource(v, collected) : toLinkage(v), + }; + } else if (v !== null && typeof v === 'object') { + // plain object without _type — treat as attribute + attrs[k] = v; + } else { + attrs[k] = v; + } + } + + if (Object.keys(attrs).length > 0) resource.attributes = attrs; + if (Object.keys(rels).length > 0) resource.relationships = rels; + } + + return linkage; +} + +/** + * Split a flat (deserialized) object into JSON:API `attributes`, + * auto-detected `relationships`, and `autoIncluded` resources. + * + * Rules (applied after skipping `_type` and `id`): + * - Scalar (string | number | boolean | null) -> `attributes`. + * - `{ _type: null }` sentinel -> relationship `{ data: null }`. + * - Plain object with `_type` and **only** `_type`+`id` keys (stub) + * -> to-one relationship linkage, nothing added to included. + * - Plain object with `_type` and **extra** fields (full inlined resource) + * -> to-one relationship linkage **+** object hoisted into `autoIncluded`. + * - Array -> to-many relationship; each element follows the same stub/full rule. + */ +function splitFlat(flat: FlatResource): { + attributes: FlatResource; + relationships: Record; + autoIncluded: JsonApiResource[]; +} { + const attributes: FlatResource = {}; + const relationships: Record = {}; + const collected = new Map(); + + for (const [key, value] of Object.entries(flat)) { + if (key === '_type' || key === 'id') continue; + + // null-relationship sentinel + if (isNullRelationship(value)) { + relationships[key] = { data: null }; + continue; + } + + // any array -> to-many relationship + if (Array.isArray(value)) { + relationships[key] = { + data: value.map((item) => { + if (!hasTypeMarker(item)) return item; + return isFullResource(item) ? hoistResource(item, collected) : toLinkage(item); + }), + }; + continue; + } + + // any plain object with _type -> to-one relationship + if (hasTypeMarker(value)) { + relationships[key] = { + data: isFullResource(value) ? hoistResource(value, collected) : toLinkage(value), + }; + continue; + } + + // plain object without _type -> to-one relationship, passed as-is + if (value !== null && typeof value === 'object') { + relationships[key] = { data: value }; + continue; + } + + // scalar (string, number, boolean, null) -> attribute + attributes[key] = value; + } + + return { attributes, relationships, autoIncluded: Array.from(collected.values()) }; +} + +// -- public API ------------------------------------------------------------- + +/** + * Serialize a flat object into a JSON:API-compliant document. + * + * - Scalar fields -> `attributes`; arrays and objects -> `relationships`. + * - Null-relationship sentinels (`{ _type: null }`) -> `{ data: null }`. + * - Explicit `relationships` in options take priority over auto-detected ones. + * - `id` is read from options first, then from the flat object's own `id` field. + * - If `included` resources are provided they are forwarded into the document. + */ +export function serialize(options: SerializeOptions): SerializeResult { + const { type, id, data: flat, relationships: explicitRels = {}, included } = options; + + const { attributes, relationships: detectedRels, autoIncluded } = splitFlat(flat); + + // Explicit relationships override auto-detected ones + const mergedRels: Record = { ...detectedRels }; + for (const [name, linkage] of Object.entries(explicitRels)) { + mergedRels[name] = { data: linkage }; + } + + // id: explicit option wins over id embedded in the flat object + const idFromFlat = + flat['id'] != null && String(flat['id']).trim() !== '' + ? String(flat['id']).trim() + : undefined; + const resolvedId = (id && id.trim() !== '' ? id.trim() : undefined) ?? idFromFlat; + + const dataBlock: Record = { type, attributes }; + if (resolvedId) dataBlock['id'] = resolvedId; + if (Object.keys(mergedRels).length > 0) dataBlock['relationships'] = mergedRels; + + const result: SerializeResult = { data: dataBlock }; + + // Merge auto-hoisted resources with explicitly provided included. + // Explicit entries win over auto-hoisted ones with the same type+id. + const explicitIncluded = included ?? []; + const explicitKeys = new Set(explicitIncluded.map((r) => `${r.type}::${r.id}`)); + const mergedIncluded = [ + ...autoIncluded.filter((r) => !explicitKeys.has(`${r.type}::${r.id}`)), + ...explicitIncluded, + ]; + if (mergedIncluded.length > 0) result.included = mergedIncluded; + + return result; +} diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts new file mode 100644 index 000000000000..bd037b7c55bd --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts @@ -0,0 +1,71 @@ +// -- JSON:API wire types ---------------------------------------------------- + +export type Linkage = { type: string; id: string }; + +export interface JsonApiResource { + type: string; + id: string; + attributes?: Record; + relationships?: Record; +} + +export interface JsonApiDocument { + data?: JsonApiResource | JsonApiResource[]; + included?: JsonApiResource[]; +} + +// -- Simplified / flat output types ---------------------------------------- + +/** + * A plain object produced by deserializing a JSON:API resource. + * + * Every field that represents a relationship carries a `_type` string marker + * (the JSON:API resource type of the related entity) so the document can be + * re-serialized back to JSON:API format without data loss. + */ +export type FlatResource = Record; + +/** Result returned by `deserialize()` — a single flat object or an array. */ +export type DeserializeResult = FlatResource | FlatResource[]; + +// -- Serialize input / output types ---------------------------------------- + +export interface SerializeOptions { + /** JSON:API resource type (e.g. "orders"). */ + type: string; + /** + * Resource id — omit for POST (create). + * When provided it overrides an `id` field present in `data`. + */ + id?: string; + /** + * Flat data object to serialize. + * + * Classification rules (applied after skipping `_type` and `id`): + * - Scalar (string | number | boolean | null) → `attributes`. + * - Any plain object → to-one `relationship`. + * Objects with `_type: null` (null-relationship sentinel) → `{ data: null }`. + * Objects with `_type` and **only** `_type`+`id` keys (stub) → linkage only. + * Objects with `_type` and **extra** fields (full inlined resource) + * → linkage in `relationships` **and** object hoisted into `included`. + * - Any array → to-many `relationship`; each element follows the same rules. + */ + data: FlatResource; + /** + * Explicit relationship map that takes priority over auto-detected ones. + * Each value is a single linkage `{ type, id }`, an array of linkages, or + * `null` to explicitly clear a to-one relationship. + */ + relationships?: Record; + /** + * Optional array of full JSON:API resource objects to embed as `included`. + * Use this to create or update related resources in a single request. + */ + included?: JsonApiResource[]; +} + +/** Document returned by `serialize()`. */ +export interface SerializeResult { + data: Record; + included?: JsonApiResource[]; +} diff --git a/packages/pieces/community/orocommerce/src/lib/common/types.ts b/packages/pieces/community/orocommerce/src/lib/common/types.ts index b6dce5570116..36d6fba7e052 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/types.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -22,6 +22,7 @@ export type OroApiCallParams = { auth: OroAuth; queryParams?: QueryParams; body?: HttpMessageBody; + headers?: Record; }; export interface OroJsonApiItem { diff --git a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts index fec4383619b3..baf68fb3414f 100644 --- a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts @@ -1,296 +1,114 @@ -export const invoiceCreateData = { - event: 'create', - channel: 'invoice', - data: { - type: 'invoices', - id: '1', - attributes: { - uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', - invoiceNumber: 'INV-2026-02-00001', - invoiceDate: '2026-02-03', - currency: 'USD', - totalAmount: '59.2100', - customerName: 'Customer A', - refCustomerId: null, - refCustomerUserId: null, - title: 'Sample Invoice', - description: 'Sample prepaid service', - memo: 'Thank you for choosing us!', - billTo: '101 Pine St, Ogdenville, USA, 44556', - shipTo: '654 Elm St, Shelbyville, USA, 09876', - shippingMethod: 'International Shipping (Tracking #: 901234)', - sellerInfo: - 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', - externalPaymentUrl: 'https://example.com/pay/123456790', - createdAt: '2026-02-03T11:33:49Z', - updatedAt: '2026-02-18T11:35:37Z', - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: 0, - timesContactedIn: 0, - timesContactedOut: 0, - paymentStatus: { - code: 'refunded', - label: 'Refunded', - isForced: false, - }, +const invoiceData = { + type: 'invoices', + id: '1', + attributes: { + uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', + invoiceNumber: 'INV-2026-02-00001', + invoiceDate: '2026-02-03', + currency: 'USD', + totalAmount: '59.2100', + customerName: 'Customer A', + refCustomerId: null, + refCustomerUserId: null, + title: 'Sample Invoice', + description: 'Sample prepaid service', + memo: 'Thank you for choosing us!', + billTo: '101 Pine St, Ogdenville, USA, 44556', + shipTo: '654 Elm St, Shelbyville, USA, 09876', + shippingMethod: 'International Shipping (Tracking #: 901234)', + sellerInfo: + 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', + externalPaymentUrl: 'https://example.com/pay/123456790', + createdAt: '2026-02-03T11:33:49Z', + updatedAt: '2026-02-18T11:35:37Z', + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: 0, + timesContactedIn: 0, + timesContactedOut: 0, + paymentStatus: { + code: 'refunded', + label: 'Refunded', + isForced: false, }, - relationships: { - lineItems: { - data: [ - { - type: 'invoicelineitems', - id: '1', - }, - { - type: 'invoicelineitems', - id: '2', - }, - { - type: 'invoicelineitems', - id: '3', - }, - ], - }, - owner: { - data: { - type: 'users', + }, + relationships: { + lineItems: { + data: [ + { + type: 'invoicelineitems', id: '1', }, - }, - organization: { - data: { - type: 'organizations', - id: '1', + { + type: 'invoicelineitems', + id: '2', }, + { + type: 'invoicelineitems', + id: '3', + }, + ], + }, + owner: { + data: { + type: 'users', + id: '1', }, - customer: { - data: null, - }, - customer_user: { - data: null, - }, - website: { - data: null, - }, - invoiceDefaultPdfFile: { - data: null, - }, - orders: { - data: [], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], + }, + organization: { + data: { + type: 'organizations', + id: '1', }, - internal_status: { - data: { - type: 'invoiceinternalstatuses', - id: 'draft', - }, + }, + customer: { + data: null, + }, + customer_user: { + data: null, + }, + website: { + data: null, + }, + invoiceDefaultPdfFile: { + data: null, + }, + orders: { + data: [], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + internal_status: { + data: { + type: 'invoiceinternalstatuses', + id: 'draft', }, }, }, +}; + +export const invoiceCreateData = { + event: 'create', + channel: 'invoice', + data: invoiceData, timestamp: 1771523549, }; export const invoiceUpdateData = { event: 'update', channel: 'invoice', - data: { - type: 'invoices', - id: '1', - attributes: { - uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', - invoiceNumber: 'INV-2026-02-00001', - invoiceDate: '2026-02-03', - currency: 'USD', - totalAmount: '59.2100', - customerName: 'Customer A', - refCustomerId: null, - refCustomerUserId: null, - title: 'Sample Invoice', - description: 'Sample prepaid service', - memo: 'Thank you for choosing us!', - billTo: '101 Pine St, Ogdenville, USA, 44556', - shipTo: '654 Elm St, Shelbyville, USA, 09876', - shippingMethod: 'International Shipping (Tracking #: 901234)', - sellerInfo: - 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', - externalPaymentUrl: 'https://example.com/pay/123456790', - createdAt: '2026-02-03T11:33:49Z', - updatedAt: '2026-02-18T11:35:37Z', - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: 0, - timesContactedIn: 0, - timesContactedOut: 0, - paymentStatus: { - code: 'refunded', - label: 'Refunded', - isForced: false, - }, - }, - relationships: { - lineItems: { - data: [ - { - type: 'invoicelineitems', - id: '1', - }, - { - type: 'invoicelineitems', - id: '2', - }, - { - type: 'invoicelineitems', - id: '3', - }, - ], - }, - owner: { - data: { - type: 'users', - id: '1', - }, - }, - organization: { - data: { - type: 'organizations', - id: '1', - }, - }, - customer: { - data: null, - }, - customer_user: { - data: null, - }, - website: { - data: null, - }, - invoiceDefaultPdfFile: { - data: null, - }, - orders: { - data: [], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - internal_status: { - data: { - type: 'invoiceinternalstatuses', - id: 'draft', - }, - }, - }, - }, + data: invoiceData, timestamp: 1771527786, }; export const invoiceDeleteData = { event: 'delete', channel: 'invoice', - data: { - type: 'invoices', - id: '1', - attributes: { - uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', - invoiceNumber: 'INV-2026-02-00001', - invoiceDate: '2026-02-03', - currency: 'USD', - totalAmount: '59.2100', - customerName: 'Customer A', - refCustomerId: null, - refCustomerUserId: null, - title: 'Sample Invoice', - description: 'Sample prepaid service', - memo: 'Thank you for choosing us!', - billTo: '101 Pine St, Ogdenville, USA, 44556', - shipTo: '654 Elm St, Shelbyville, USA, 09876', - shippingMethod: 'International Shipping (Tracking #: 901234)', - sellerInfo: - 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', - externalPaymentUrl: 'https://example.com/pay/123456790', - createdAt: '2026-02-03T11:33:49Z', - updatedAt: '2026-02-18T11:35:37Z', - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: 0, - timesContactedIn: 0, - timesContactedOut: 0, - paymentStatus: { - code: 'refunded', - label: 'Refunded', - isForced: false, - }, - }, - relationships: { - lineItems: { - data: [ - { - type: 'invoicelineitems', - id: '1', - }, - { - type: 'invoicelineitems', - id: '2', - }, - { - type: 'invoicelineitems', - id: '3', - }, - ], - }, - owner: { - data: { - type: 'users', - id: '1', - }, - }, - organization: { - data: { - type: 'organizations', - id: '1', - }, - }, - customer: { - data: null, - }, - customer_user: { - data: null, - }, - website: { - data: null, - }, - invoiceDefaultPdfFile: { - data: null, - }, - orders: { - data: [], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - internal_status: { - data: { - type: 'invoiceinternalstatuses', - id: 'draft', - }, - }, - }, - }, + data: invoiceData, timestamp: 1771527301, }; From ce6182bfcc70089ec9e3c6ac478ea6e6e42c2cda Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 25 Feb 2026 18:28:43 +0100 Subject: [PATCH 08/50] feat(orocommerce): added i18n, improved examples to match orocommerce payload --- .../community/orocommerce/src/i18n/de.json | 192 +++ .../community/orocommerce/src/i18n/fr.json | 191 +++ .../community/orocommerce/src/i18n/nl.json | 192 +++ .../community/orocommerce/src/i18n/pl.json | 191 +++ .../orocommerce/src/i18n/translation.json | 191 +++ .../community/orocommerce/src/i18n/uk.json | 191 +++ .../orocommerce/src/lib/examples/invoice.ts | 173 +-- .../orocommerce/src/lib/examples/order.ts | 1095 +++++++++-------- 8 files changed, 1788 insertions(+), 628 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/i18n/de.json create mode 100644 packages/pieces/community/orocommerce/src/i18n/fr.json create mode 100644 packages/pieces/community/orocommerce/src/i18n/nl.json create mode 100644 packages/pieces/community/orocommerce/src/i18n/pl.json create mode 100644 packages/pieces/community/orocommerce/src/i18n/translation.json create mode 100644 packages/pieces/community/orocommerce/src/i18n/uk.json diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json new file mode 100644 index 000000000000..ddff5e6cd879 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -0,0 +1,192 @@ +{ + "B2B digital commerce solution": "B2B-Digitalkommerzlösung", + "Server URL": "Server-URL", + "Admin Prefix": "Admin-Präfix", + "Client ID": "Client-ID", + "Client Secret": "Client-Secret", + "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "Die Basis-URL Ihrer OroCommerce-Instanz (z. B. https://your-store.com).", + "The admin panel URL prefix (default is \"admin\").": "Das URL-Präfix des Adminbereichs (Standard ist \"admin\").", + "The OAuth Client ID from your OroCommerce OAuth application.": "Die OAuth-Client-ID aus Ihrer OroCommerce-OAuth-Anwendung.", + "The OAuth Client Secret from your OroCommerce OAuth application.": "Das OAuth-Client-Secret aus Ihrer OroCommerce-OAuth-Anwendung.", + "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthentifizieren Sie sich bei den OroCommerce-APIs mit OAuth 2.0 Client Credentials.\n\n**Schritte zum Abrufen der Zugangsdaten:**\n1. Melden Sie sich in Ihrem OroCommerce-Administrationsbereich an.\n2. Navigieren Sie zu **System** > **User Management** > **OAuth Applications**.\n3. Klicken Sie auf **Create OAuth Application** und konfigurieren Sie:\n - **Application Name**: Geben Sie einen aussagekräftigen Namen ein (z. B. \"Activepieces Integration\")\n - **Grants**: Wählen Sie **Client Credentials**\n - **Redirect URIs**: Für den Client-Credentials-Flow nicht erforderlich\n4. Speichern Sie die Anwendung und kopieren Sie die **Client ID** und das **Client Secret**.\n5. Notieren Sie Ihre **Server URL** (z. B. `https://your-store.com`) und das **Admin Prefix** (in der Regel `admin`).\n ", + "Create Invoice": "Rechnung erstellen", + "Create Order": "Bestellung erstellen", + "API Call": "API-Aufruf", + "Serialize JSON:API Request": "JSON:API-Anfrage serialisieren", + "Unserialize JSON:API Response": "JSON:API-Antwort deserialisieren", + "Creates a new invoice record in OroCommerce.": "Erstellt einen neuen Rechnungsdatensatz in OroCommerce.", + "Creates a new order record in OroCommerce.": "Erstellt einen neuen Bestelldatensatz in OroCommerce.", + "Make a direct authenticated call to the OroCommerce JSON:API.": "Führt einen direkten authentifizierten Aufruf der OroCommerce JSON:API aus.", + "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Wandelt ein einfaches Objekt in einen JSON:API-konformen Anfrage-Body um. Beziehungsfelder werden automatisch erkannt – jede Array- oder Objektvariable wird als Beziehung behandelt, nur Skalare werden zu Attributen. Das Ergebnis kann direkt als Anfrage-Body der API-Aufruf-Aktion verwendet werden.", + "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Macht einen JSON:API-Antwort-Body flach (einfaches Objekt oder Array von Objekten). Eingeschlossene Beziehungen werden aufgelöst und inline eingefügt. Nicht aufgelöste Beziehungsreferenzen werden als { _type, id }-Stubs beibehalten, sodass das Dokument ohne Datenverlust wieder in eine gültige JSON:API-Anfrage serialisiert werden kann. Übergeben Sie die Ausgabe \"body\" der API-Aufruf-Aktion als Response-Eingabe.", + "Invoice Date": "Rechnungsdatum", + "Currency": "Währung", + "Customer Name": "Kundenname", + "Customer": "Kunde", + "Customer: Raw ID": "Kunde: Roh-ID", + "Customer User": "Kundenbenutzer", + "Customer User: Raw ID": "Kundenbenutzer: Roh-ID", + "External Customer ID": "Externe Kunden-ID", + "External Customer User ID": "Externe Kundenbenutzer-ID", + "Total Amount": "Gesamtbetrag", + "Invoice Number": "Rechnungsnummer", + "Title": "Titel", + "Description": "Beschreibung", + "Memo": "Notiz", + "Bill To": "Rechnung an", + "Ship To": "Lieferadresse", + "Shipping Method": "Versandmethode", + "Seller Info": "Verkäuferinformationen", + "External Payment URL": "Externe Zahlungs-URL", + "Organization": "Organisation", + "Organization: Raw ID": "Organisation: Roh-ID", + "User": "Benutzer", + "Owner: Raw ID": "Inhaber: Roh-ID", + "Website": "Website", + "Website: Raw ID": "Website: Roh-ID", + "Internal Status": "Interner Status", + "Internal Status: Raw ID": "Interner Status: Roh-ID", + "Line Items": "Positionen", + "Identifier": "Bezeichner", + "PO Number": "Bestellnummer (PO)", + "Customer Notes": "Kundennotizen", + "Ship Until Date": "Versenden bis Datum", + "Overridden Shipping Cost": "Überschriebene Versandkosten", + "Estimated Shipping Cost": "Geschätzte Versandkosten", + "Shipping Method Type": "Typ der Versandmethode", + "Disable Promotions": "Aktionen deaktivieren", + "Payment Term": "Zahlungsbedingung", + "Payment Term: Raw ID": "Zahlungsbedingung: Roh-ID", + "Warehouse": "Lager", + "Warehouse: Raw ID": "Lager: Roh-ID", + "Parent Order": "Übergeordnete Bestellung", + "Parent Order: Raw ID": "Übergeordnete Bestellung: Roh-ID", + "Status": "Status", + "Status: Raw ID": "Status: Roh-ID", + "Billing: Label": "Rechnung: Bezeichnung", + "Billing: First Name": "Rechnung: Vorname", + "Billing: Last Name": "Rechnung: Nachname", + "Billing: Organization": "Rechnung: Organisation", + "Billing: Phone": "Rechnung: Telefon", + "Billing: Street": "Rechnung: Straße", + "Billing: Street 2": "Rechnung: Straße 2", + "Billing: City": "Rechnung: Stadt", + "Billing: Postal Code": "Rechnung: Postleitzahl", + "Billing: Country": "Rechnung: Land", + "Billing: Country Raw ID": "Rechnung: Land Roh-ID", + "Billing: Region / State": "Rechnung: Region / Bundesland", + "Billing: Region Raw ID": "Rechnung: Region Roh-ID", + "Billing: Custom Region": "Rechnung: Benutzerdefinierte Region", + "Shipping: Label": "Versand: Bezeichnung", + "Shipping: First Name": "Versand: Vorname", + "Shipping: Last Name": "Versand: Nachname", + "Shipping: Organization": "Versand: Organisation", + "Shipping: Phone": "Versand: Telefon", + "Shipping: Street": "Versand: Straße", + "Shipping: Street 2": "Versand: Straße 2", + "Shipping: City": "Versand: Stadt", + "Shipping: Postal Code": "Versand: Postleitzahl", + "Shipping: Country": "Versand: Land", + "Shipping: Country Raw ID": "Versand: Land Roh-ID", + "Shipping: Region / State": "Versand: Region / Bundesland", + "Shipping: Region Raw ID": "Versand: Region Roh-ID", + "Shipping: Custom Region": "Versand: Benutzerdefinierte Region", + "Product": "Produkt", + "Method": "Methode", + "Resource URI": "Ressourcen-URI", + "Headers": "Header", + "Query Parameters": "Abfrageparameter", + "Request Body": "Anfrage-Body", + "Resource Type": "Ressourcentyp", + "Resource ID": "Ressourcen-ID", + "Attributes": "Attribute", + "Relationships (override)": "Beziehungen (Überschreiben)", + "Included": "Included", + "JSON:API Response": "JSON:API-Antwort", + "Invoice date in YYYY-MM-DD format.": "Rechnungsdatum im Format YYYY-MM-DD.", + "ISO-4217 3-letter currency code (e.g. USD, EUR).": "ISO-4217-Währungscode mit 3 Buchstaben (z. B. USD, EUR).", + "Name of the company being billed. Stored as a plain-text label on the invoice.": "Name des Unternehmens, das belastet wird. Wird als Klartextbezeichnung auf der Rechnung gespeichert.", + "Select a customer.": "Wählen Sie einen Kunden aus.", + "Overrides the Customer dropdown. Paste an ID from a previous step.": "Überschreibt das Dropdown \"Kunde\". Fügen Sie eine ID aus einem vorherigen Schritt ein.", + "Select a Customer User.": "Wählen Sie einen Kundenbenutzer aus.", + "Overrides the Customer User dropdown.": "Überschreibt das Dropdown \"Kundenbenutzer\".", + "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Optionale ID-Referenz zu einem Kunden. Kann zum Speichern einer beliebigen externen ID verwendet werden.", + "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Optionale ID-Referenz zu einem Kundenbenutzer. Kann zum Speichern einer beliebigen externen ID verwendet werden.", + "Total invoice amount. Should equal the sum of all line item row totals.": "Gesamtrechnungsbetrag. Sollte der Summe aller Positionszeilen entsprechen.", + "Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.": "Fortlaufende Rechnungsnummer (z. B. INV-2026-00001). Wird automatisch erzeugt, wenn leer gelassen.", + "Alternative invoice title that reflects its nature.": "Alternativer Rechnungstitel, der ihren Charakter widerspiegelt.", + "Internal description of the invoice.": "Interne Beschreibung der Rechnung.", + "Short memo visible on the invoice (e.g. \"Thank you!\").": "Kurze Notiz, die auf der Rechnung sichtbar ist (z. B. \"Thank you!\").", + "Billing address HTML string (e.g. 123 Main St, City, Country).": "HTML-Zeichenkette der Rechnungsadresse (z. B. 123 Main St, Stadt, Land).", + "Shipping address HTML string.": "HTML-Zeichenkette der Lieferadresse.", + "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Beschriftung der Versandmethode (z. B. \"International Shipping (Tracking #: 123)\").", + "Seller contact / address HTML string.": "HTML-Zeichenkette für Verkäuferkontakt/-adresse.", + "URL for the external payment page.": "URL für die externe Zahlungsseite.", + "Search organizations.": "Organisationen suchen.", + "Overrides the Organization dropdown.": "Überschreibt das Dropdown \"Organisation\".", + "Search user by name, username or email.": "Benutzer nach Name, Benutzername oder E-Mail suchen.", + "Overrides the Owner dropdown.": "Überschreibt das Dropdown \"Inhaber\".", + "Search websites.": "Websites suchen.", + "Overrides the Website dropdown.": "Überschreibt das Dropdown \"Website\".", + "Invoice internal status (e.g. Draft, Open).": "Interner Rechnungsstatus (z. B. Draft, Open).", + "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Überschreibt das Dropdown \"Interner Status\" (z. B. draft, open, paid).", + "Invoice line items. Each item is sent via JSON:API included.": "Rechnungspositionen. Jedes Element wird per JSON:API included übertragen.", + "The customer this order belongs to.": "Der Kunde, zu dem diese Bestellung gehört.", + "Unique order reference (e.g. FR1012401Z).": "Eindeutige Bestellreferenz (z. B. FR1012401Z).", + "Purchase order number provided by the buyer.": "Vom Käufer angegebene Bestellnummer.", + "Notes from the customer (e.g. \"Call before delivery\").": "Notizen des Kunden (z. B. \"Call before delivery\").", + "Latest acceptable ship date in YYYY-MM-DD format.": "Spätestes akzeptables Versanddatum im Format YYYY-MM-DD.", + "Custom shipping cost that overrides the calculated value.": "Benutzerdefinierte Versandkosten, die den berechneten Wert überschreiben.", + "Shipping cost calculated from the selected shipping method.": "Versandkosten, die anhand der ausgewählten Versandmethode berechnet werden.", + "The shipping method selected for the order (e.g. \"flat_rate_2\").": "Die für die Bestellung ausgewählte Versandmethode (z. B. \"flat_rate_2\").", + "The shipping method type (e.g. \"primary\").": "Der Typ der Versandmethode (z. B. \"primary\").", + "Prevent the promotions engine from running for this order.": "Verhindern, dass die Aktions-Engine für diese Bestellung ausgeführt wird.", + "Order internal status (e.g. Open, Cancelled).": "Interner Bestellstatus (z. B. Open, Cancelled).", + "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Überschreibt das Dropdown \"Interner Status\" (z. B. order_internal_status.open).", + "Search payment terms.": "Zahlungsbedingungen suchen.", + "Overrides the Payment Term dropdown.": "Überschreibt das Dropdown \"Zahlungsbedingung\".", + "Search warehouses.": "Lager suchen.", + "Overrides the Warehouse dropdown.": "Überschreibt das Dropdown \"Lager\".", + "Search orders to use as the parent order.": "Bestellungen suchen, die als übergeordnete Bestellung verwendet werden sollen.", + "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Überschreibt das Dropdown \"Übergeordnete Bestellung\". Fügen Sie eine Bestell-ID aus einem vorherigen Schritt ein.", + "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Bestellstatus, der von einem externen System verwaltet wird (nur relevant, wenn \"Enable External Status Management\" aktiviert ist).", + "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Überschreibt das Dropdown \"Status\". Wird nur angewendet, wenn \"Enable External Status Management\" aktiviert ist.", + "Address label (e.g. \"Main Office\").": "Adressbezeichnung (z. B. \"Main Office\").", + "ISO-3166 country. Start typing to filter the list.": "ISO-3166-Land. Beginnen Sie zu tippen, um die Liste zu filtern.", + "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Überschreibt das Dropdown \"Land (Rechnung)\" (ISO-3166-Ländercode mit 2 Buchstaben, z. B. US).", + "Region or state. Select a country first. Start typing to filter.": "Region oder Bundesland. Wählen Sie zuerst ein Land aus. Beginnen Sie zu tippen, um zu filtern.", + "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Überschreibt das Dropdown \"Region (Rechnung)\" (ISO-3166-2-Code, z. B. US-NY).", + "Free-text region for countries without predefined regions.": "Freitextregion für Länder ohne vordefinierte Regionen.", + "Address label (e.g. \"Warehouse East\").": "Adressbezeichnung (z. B. \"Warehouse East\").", + "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Überschreibt das Dropdown \"Land (Versand)\" (ISO-3166-Ländercode mit 2 Buchstaben, z. B. US).", + "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Überschreibt das Dropdown \"Region (Versand)\" (ISO-3166-2-Code, z. B. US-NY).", + "Order line items.": "Bestellpositionen.", + "Search products.": "Produkte suchen.", + "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Pfad relativ zur API-Basis, z. B. /orders, /orders/42, /invoices?filter[status]=open", + "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Anfrage-Header als JSON-Objekt. Standardwerte umfassen Accept und X-Include für JSON:API.", + "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Zusätzliche Abfrageparameter als JSON-Objekt, z. B. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", + "Request body as a JSON object (used for POST / PATCH).": "Anfrage-Body als JSON-Objekt (wird für POST / PATCH verwendet).", + "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Der JSON:API-Ressourcentyp (z. B. orders, invoices, products). Das Feld _type wird automatisch als Ressourcentyp verwendet.", + "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID der Ressource – lassen Sie dieses Feld beim Erstellen eines neuen Datensatzes (POST) leer oder wenn die Eingabe ein id-Feld enthält (das id-Feld wird automatisch übernommen).", + "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Das flache Objekt, das serialisiert werden soll. Übergeben Sie hier das vollständige JSON-Dokument – Arrays und Objekte werden automatisch in \"relationships\" verschoben; nur Skalare werden zu Attributen. Für eine manuelle Erstellung verwenden Sie einfache Schlüssel/Wert-Paare, z. B. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", + "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Optionale explizite Beziehungszuordnung, die Vorrang vor allen in Attributes erkannten Beziehungen hat. Jeder Schlüssel ist ein Beziehungsname; der Wert ist ein einzelnes Verknüpfungsobjekt {\"type\":\"customers\",\"id\":\"42\"}, ein Array von Verknüpfungsobjekten oder null. Beispiel: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", + "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Optionale Liste vollständiger JSON:API-Ressourcenobjekte, die im Abschnitt \"included\" der Anfrage eingebettet werden. Wenn die Eingabe von Attributes bereits ein \"included\"-Array enthält (z. B. den Roh-Response-Body des API-Aufrufs), wird es automatisch weitergegeben – dieses Feld muss nur gesetzt werden, wenn eingeschlossene Ressourcen manuell hinzugefügt oder überschrieben werden. Jeder Eintrag muss eine gültige JSON:API-Ressource mit den Feldern \"type\", \"id\" und \"attributes\" sein.", + "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Der rohe JSON:API-Antwort-Body – das Feld \"body\" aus der Ausgabe der API-Aufruf-Aktion. Muss einen obersten Schlüssel \"data\" enthalten (einzelne Ressource oder Sammlung).", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "DELETE": "DELETE", + "New Order": "Neue Bestellung", + "Order Removal": "Bestellung entfernen", + "Order Update": "Bestellung aktualisieren", + "New Invoice": "Neue Rechnung", + "Invoice Removal": "Rechnung entfernen", + "Invoice Update": "Rechnung aktualisieren", + "Triggered when a new order is created": "Ausgelöst, wenn eine neue Bestellung erstellt wird", + "Triggered when an order is deleted": "Ausgelöst, wenn eine Bestellung gelöscht wird", + "Triggered when an order is updated": "Ausgelöst, wenn eine Bestellung aktualisiert wird", + "Triggered when a new invoice is created": "Ausgelöst, wenn eine neue Rechnung erstellt wird", + "Triggered when an invoice is deleted": "Ausgelöst, wenn eine Rechnung gelöscht wird", + "Triggered when an invoice is updated": "Ausgelöst, wenn eine Rechnung aktualisiert wird" +} + diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json new file mode 100644 index 000000000000..a69b0e4a51eb --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -0,0 +1,191 @@ +{ + "B2B digital commerce solution": "Solution de commerce numérique B2B", + "Server URL": "URL du serveur", + "Admin Prefix": "Préfixe d'administration", + "Client ID": "ID client", + "Client Secret": "Secret client", + "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "L'URL de base de votre instance OroCommerce (par ex. https://your-store.com).", + "The admin panel URL prefix (default is \"admin\").": "Le préfixe d'URL du panneau d'administration (valeur par défaut \"admin\").", + "The OAuth Client ID from your OroCommerce OAuth application.": "L'ID client OAuth de votre application OAuth OroCommerce.", + "The OAuth Client Secret from your OroCommerce OAuth application.": "Le secret client OAuth de votre application OAuth OroCommerce.", + "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthentifiez-vous auprès des API OroCommerce en utilisant les identifiants client OAuth 2.0.\n\n**Étapes pour obtenir les identifiants :**\n1. Connectez-vous à votre panneau d'administration OroCommerce.\n2. Accédez à **System** > **User Management** > **OAuth Applications**.\n3. Cliquez sur **Create OAuth Application** et configurez :\n - **Application Name** : Saisissez un nom descriptif (par ex. \"Activepieces Integration\")\n - **Grants** : Sélectionnez **Client Credentials**\n - **Redirect URIs** : Non requis pour le flux Client Credentials\n4. Enregistrez l'application et copiez le **Client ID** et le **Client Secret**.\n5. Notez votre **Server URL** (par ex. `https://your-store.com`) et le **Admin Prefix** (généralement `admin`).\n ", + "Create Invoice": "Créer une facture", + "Create Order": "Créer une commande", + "API Call": "Appel d'API", + "Serialize JSON:API Request": "Sérialiser la requête JSON:API", + "Unserialize JSON:API Response": "Désérialiser la réponse JSON:API", + "Creates a new invoice record in OroCommerce.": "Crée un nouvel enregistrement de facture dans OroCommerce.", + "Creates a new order record in OroCommerce.": "Crée un nouvel enregistrement de commande dans OroCommerce.", + "Make a direct authenticated call to the OroCommerce JSON:API.": "Effectue un appel authentifié direct vers le JSON:API d'OroCommerce.", + "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Convertit un objet simple en corps de requête conforme à JSON:API. Les champs de relation sont détectés automatiquement : toute valeur de type tableau ou objet est traitée comme une relation, seuls les scalaires deviennent des attributs. Le résultat peut être utilisé directement comme corps de requête de l'action Appel d'API.", + "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Aplati un corps de réponse JSON:API en un objet simple (ou un tableau d'objets). Les relations incluses sont résolues et intégrées. Les références de relations non résolues sont conservées sous forme de stubs { _type, id } afin que le document puisse être à nouveau sérialisé en une requête JSON:API valide sans perte de données. Passez la sortie \"body\" de l'action Appel d'API comme entrée Response.", + "Invoice Date": "Date de facture", + "Currency": "Devise", + "Customer Name": "Nom du client", + "Customer": "Client", + "Customer: Raw ID": "Client : ID brut", + "Customer User": "Utilisateur client", + "Customer User: Raw ID": "Utilisateur client : ID brut", + "External Customer ID": "ID client externe", + "External Customer User ID": "ID d'utilisateur client externe", + "Total Amount": "Montant total", + "Invoice Number": "Numéro de facture", + "Title": "Titre", + "Description": "Description", + "Memo": "Mémo", + "Bill To": "Adresse de facturation", + "Ship To": "Adresse de livraison", + "Shipping Method": "Méthode d'expédition", + "Seller Info": "Informations vendeur", + "External Payment URL": "URL de paiement externe", + "Organization": "Organisation", + "Organization: Raw ID": "Organisation : ID brut", + "User": "Utilisateur", + "Owner: Raw ID": "Propriétaire : ID brut", + "Website": "Site web", + "Website: Raw ID": "Site web : ID brut", + "Internal Status": "Statut interne", + "Internal Status: Raw ID": "Statut interne : ID brut", + "Line Items": "Lignes", + "Identifier": "Identifiant", + "PO Number": "Numéro de commande (PO)", + "Customer Notes": "Notes client", + "Ship Until Date": "Date limite d'expédition", + "Overridden Shipping Cost": "Coût d'expédition écrasé", + "Estimated Shipping Cost": "Coût d'expédition estimé", + "Shipping Method Type": "Type de méthode d'expédition", + "Disable Promotions": "Désactiver les promotions", + "Payment Term": "Condition de paiement", + "Payment Term: Raw ID": "Condition de paiement : ID brut", + "Warehouse": "Entrepôt", + "Warehouse: Raw ID": "Entrepôt : ID brut", + "Parent Order": "Commande parente", + "Parent Order: Raw ID": "Commande parente : ID brut", + "Status": "Statut", + "Status: Raw ID": "Statut : ID brut", + "Billing: Label": "Facturation : libellé", + "Billing: First Name": "Facturation : prénom", + "Billing: Last Name": "Facturation : nom", + "Billing: Organization": "Facturation : organisation", + "Billing: Phone": "Facturation : téléphone", + "Billing: Street": "Facturation : rue", + "Billing: Street 2": "Facturation : rue 2", + "Billing: City": "Facturation : ville", + "Billing: Postal Code": "Facturation : code postal", + "Billing: Country": "Facturation : pays", + "Billing: Country Raw ID": "Facturation : pays ID brut", + "Billing: Region / State": "Facturation : région / État", + "Billing: Region Raw ID": "Facturation : région ID brut", + "Billing: Custom Region": "Facturation : région personnalisée", + "Shipping: Label": "Expédition : libellé", + "Shipping: First Name": "Expédition : prénom", + "Shipping: Last Name": "Expédition : nom", + "Shipping: Organization": "Expédition : organisation", + "Shipping: Phone": "Expédition : téléphone", + "Shipping: Street": "Expédition : rue", + "Shipping: Street 2": "Expédition : rue 2", + "Shipping: City": "Expédition : ville", + "Shipping: Postal Code": "Expédition : code postal", + "Shipping: Country": "Expédition : pays", + "Shipping: Country Raw ID": "Expédition : pays ID brut", + "Shipping: Region / State": "Expédition : région / État", + "Shipping: Region Raw ID": "Expédition : région ID brut", + "Shipping: Custom Region": "Expédition : région personnalisée", + "Product": "Produit", + "Method": "Méthode", + "Resource URI": "URI de ressource", + "Headers": "En-têtes", + "Query Parameters": "Paramètres de requête", + "Request Body": "Corps de la requête", + "Resource Type": "Type de ressource", + "Resource ID": "ID de ressource", + "Attributes": "Attributs", + "Relationships (override)": "Relations (remplacement)", + "Included": "Inclus", + "JSON:API Response": "Réponse JSON:API", + "Invoice date in YYYY-MM-DD format.": "Date de facture au format YYYY-MM-DD.", + "ISO-4217 3-letter currency code (e.g. USD, EUR).": "Code de devise ISO-4217 à 3 lettres (par ex. USD, EUR).", + "Name of the company being billed. Stored as a plain-text label on the invoice.": "Nom de l'entreprise facturée. Enregistré comme libellé en texte brut sur la facture.", + "Select a customer.": "Sélectionnez un client.", + "Overrides the Customer dropdown. Paste an ID from a previous step.": "Remplace la liste déroulante Client. Collez un ID provenant d'une étape précédente.", + "Select a Customer User.": "Sélectionnez un utilisateur client.", + "Overrides the Customer User dropdown.": "Remplace la liste déroulante Utilisateur client.", + "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Référence ID facultative vers un client. Peut être utilisée pour stocker un ID externe arbitraire.", + "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Référence ID facultative vers un utilisateur client. Peut être utilisée pour stocker un ID externe arbitraire.", + "Total invoice amount. Should equal the sum of all line item row totals.": "Montant total de la facture. Doit être égal à la somme de toutes les lignes.", + "Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.": "Numéro de facture séquentiel (par ex. INV-2026-00001). Généré automatiquement si laissé vide.", + "Alternative invoice title that reflects its nature.": "Titre de facture alternatif reflétant sa nature.", + "Internal description of the invoice.": "Description interne de la facture.", + "Short memo visible on the invoice (e.g. \"Thank you!\").": "Court mémo visible sur la facture (par ex. \"Thank you!\").", + "Billing address HTML string (e.g. 123 Main St, City, Country).": "Chaîne HTML de l'adresse de facturation (par ex. 123 Main St, ville, pays).", + "Shipping address HTML string.": "Chaîne HTML de l'adresse de livraison.", + "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Libellé de la méthode d'expédition (par ex. \"International Shipping (Tracking #: 123)\").", + "Seller contact / address HTML string.": "Chaîne HTML du contact/adresse du vendeur.", + "URL for the external payment page.": "URL de la page de paiement externe.", + "Search organizations.": "Rechercher des organisations.", + "Overrides the Organization dropdown.": "Remplace la liste déroulante Organisation.", + "Search user by name, username or email.": "Rechercher un utilisateur par nom, nom d'utilisateur ou e-mail.", + "Overrides the Owner dropdown.": "Remplace la liste déroulante Propriétaire.", + "Search websites.": "Rechercher des sites web.", + "Overrides the Website dropdown.": "Remplace la liste déroulante Site web.", + "Invoice internal status (e.g. Draft, Open).": "Statut interne de la facture (par ex. Draft, Open).", + "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Remplace la liste déroulante Statut interne (par ex. draft, open, paid).", + "Invoice line items. Each item is sent via JSON:API included.": "Lignes de facture. Chaque élément est envoyé via JSON:API included.", + "The customer this order belongs to.": "Le client auquel appartient cette commande.", + "Unique order reference (e.g. FR1012401Z).": "Référence de commande unique (par ex. FR1012401Z).", + "Purchase order number provided by the buyer.": "Numéro de commande d'achat fourni par l'acheteur.", + "Notes from the customer (e.g. \"Call before delivery\").": "Notes du client (par ex. \"Call before delivery\").", + "Latest acceptable ship date in YYYY-MM-DD format.": "Date d'expédition maximale acceptable au format YYYY-MM-DD.", + "Custom shipping cost that overrides the calculated value.": "Coût d'expédition personnalisé qui remplace la valeur calculée.", + "Shipping cost calculated from the selected shipping method.": "Coût d'expédition calculé à partir de la méthode d'expédition sélectionnée.", + "The shipping method selected for the order (e.g. \"flat_rate_2\").": "La méthode d'expédition sélectionnée pour la commande (par ex. \"flat_rate_2\").", + "The shipping method type (e.g. \"primary\").": "Le type de méthode d'expédition (par ex. \"primary\").", + "Prevent the promotions engine from running for this order.": "Empêcher le moteur de promotions de s'exécuter pour cette commande.", + "Order internal status (e.g. Open, Cancelled).": "Statut interne de la commande (par ex. Open, Cancelled).", + "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Remplace la liste déroulante Statut interne (par ex. order_internal_status.open).", + "Search payment terms.": "Rechercher des conditions de paiement.", + "Overrides the Payment Term dropdown.": "Remplace la liste déroulante Condition de paiement.", + "Search warehouses.": "Rechercher des entrepôts.", + "Overrides the Warehouse dropdown.": "Remplace la liste déroulante Entrepôt.", + "Search orders to use as the parent order.": "Rechercher des commandes à utiliser comme commande parente.", + "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Remplace la liste déroulante Commande parente. Collez un ID de commande d'une étape précédente.", + "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Statut de commande géré par un système externe (pertinent uniquement lorsque \"Enable External Status Management\" est activé).", + "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Remplace la liste déroulante Statut. Appliqué uniquement lorsque \"Enable External Status Management\" est activé.", + "Address label (e.g. \"Main Office\").": "Libellé d'adresse (par ex. \"Main Office\").", + "ISO-3166 country. Start typing to filter the list.": "Pays ISO-3166. Commencez à taper pour filtrer la liste.", + "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Remplace la liste déroulante Pays de facturation (code pays ISO-3166 à 2 lettres, par ex. US).", + "Region or state. Select a country first. Start typing to filter.": "Région ou État. Sélectionnez d'abord un pays. Commencez à taper pour filtrer.", + "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Remplace la liste déroulante Région de facturation (code ISO 3166-2, par ex. US-NY).", + "Free-text region for countries without predefined regions.": "Région en texte libre pour les pays sans régions prédéfinies.", + "Address label (e.g. \"Warehouse East\").": "Libellé d'adresse (par ex. \"Warehouse East\").", + "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Remplace la liste déroulante Pays d'expédition (code pays ISO-3166 à 2 lettres, par ex. US).", + "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Remplace la liste déroulante Région d'expédition (code ISO 3166-2, par ex. US-NY).", + "Order line items.": "Lignes de commande.", + "Search products.": "Rechercher des produits.", + "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Chemin relatif à la base de l'API, par ex. /orders, /orders/42, /invoices?filter[status]=open", + "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "En-têtes de requête sous forme d'objet JSON. Les valeurs par défaut incluent Accept et X-Include pour JSON:API.", + "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Paramètres de requête supplémentaires sous forme d'objet JSON, par ex. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", + "Request body as a JSON object (used for POST / PATCH).": "Corps de requête sous forme d'objet JSON (utilisé pour POST / PATCH).", + "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Le type de ressource JSON:API (par ex. orders, invoices, products). Le champ _type est automatiquement utilisé comme type de ressource.", + "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID de la ressource - laissez vide lors de la création d'un nouvel enregistrement (POST) ou lorsque l'entrée contient un champ id (le champ id est repris automatiquement).", + "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "L'objet plat à sérialiser. Passez ici le document JSON complet : les tableaux et objets sont automatiquement placés dans \"relationships\" ; seuls les scalaires deviennent des attributs. Pour une construction manuelle, utilisez des paires clé/valeur simples, par ex. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", + "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Carte de relations explicite facultative qui a priorité sur toute relation détectée dans Attributes. Chaque clé est un nom de relation ; la valeur est un objet de liaison unique {\"type\":\"customers\",\"id\":\"42\"}, un tableau d'objets de liaison ou null. Exemple : {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", + "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Tableau facultatif d'objets ressource JSON:API complets à intégrer dans la section \"included\" de la requête. Si l'entrée Attributes contient déjà un tableau \"included\" (par ex. le corps brut de la réponse Appel d'API), il sera transmis automatiquement ; ce champ ne doit être renseigné que lors de l'ajout ou du remplacement manuel de ressources incluses. Chaque élément doit être une ressource JSON:API valide avec les champs \"type\", \"id\" et \"attributes\".", + "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Le corps brut de la réponse JSON:API - le champ \"body\" de la sortie de l'action Appel d'API. Doit contenir une clé de premier niveau \"data\" (ressource unique ou collection).", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "DELETE": "DELETE", + "New Order": "Nouvelle commande", + "Order Removal": "Suppression de commande", + "Order Update": "Mise à jour de commande", + "New Invoice": "Nouvelle facture", + "Invoice Removal": "Suppression de facture", + "Invoice Update": "Mise à jour de facture", + "Triggered when a new order is created": "Déclenché lorsqu'une nouvelle commande est créée", + "Triggered when an order is deleted": "Déclenché lorsqu'une commande est supprimée", + "Triggered when an order is updated": "Déclenché lorsqu'une commande est mise à jour", + "Triggered when a new invoice is created": "Déclenché lorsqu'une nouvelle facture est créée", + "Triggered when an invoice is deleted": "Déclenché lorsqu'une facture est supprimée", + "Triggered when an invoice is updated": "Déclenché lorsqu'une facture est mise à jour" +} diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json new file mode 100644 index 000000000000..cf9412c8e3e1 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -0,0 +1,192 @@ +{ + "B2B digital commerce solution": "B2B digitale commerce-oplossing", + "Server URL": "Server-URL", + "Admin Prefix": "Admin-prefix", + "Client ID": "Client-ID", + "Client Secret": "Clientgeheim", + "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "De basis-URL van uw OroCommerce-instantie (bijv. https://your-store.com).", + "The admin panel URL prefix (default is \"admin\").": "Het URL-prefix van het beheerpaneel (standaard \"admin\").", + "The OAuth Client ID from your OroCommerce OAuth application.": "De OAuth Client-ID uit uw OroCommerce OAuth-applicatie.", + "The OAuth Client Secret from your OroCommerce OAuth application.": "Het OAuth Clientgeheim uit uw OroCommerce OAuth-applicatie.", + "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthenticeer bij de OroCommerce-API's met OAuth 2.0 Client Credentials.\n\n**Stappen om referenties te verkrijgen:**\n1. Meld u aan bij uw OroCommerce-beheerpaneel.\n2. Ga naar **System** > **User Management** > **OAuth Applications**.\n3. Klik op **Create OAuth Application** en configureer:\n - **Application Name**: Voer een beschrijvende naam in (bijv. \"Activepieces Integration\")\n - **Grants**: Selecteer **Client Credentials**\n - **Redirect URIs**: Niet vereist voor de Client Credentials-flow\n4. Sla de applicatie op en kopieer de **Client ID** en **Client Secret**.\n5. Noteer uw **Server URL** (bijv. `https://your-store.com`) en **Admin Prefix** (meestal `admin`).\n ", + "Create Invoice": "Factuur aanmaken", + "Create Order": "Order aanmaken", + "API Call": "API-oproep", + "Serialize JSON:API Request": "JSON:API-verzoek serialiseren", + "Unserialize JSON:API Response": "JSON:API-respons deserialiseren", + "Creates a new invoice record in OroCommerce.": "Maakt een nieuw factuurrecord aan in OroCommerce.", + "Creates a new order record in OroCommerce.": "Maakt een nieuw orderrecord aan in OroCommerce.", + "Make a direct authenticated call to the OroCommerce JSON:API.": "Voer een directe geauthenticeerde oproep uit naar de OroCommerce JSON:API.", + "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Zet een eenvoudig object om in een JSON:API-conforme requestbody. Relatievelden worden automatisch gedetecteerd: elke array- of objectwaarde wordt als relatie behandeld, alleen scalars worden attributen. Het resultaat kan direct worden gebruikt als requestbody van de API Call-actie.", + "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Maakt een JSON:API-responsbody vlak (naar een eenvoudig object of een array van objecten). Inbegrepen relaties worden opgelost en inline opgenomen. Onopgeloste referenties naar relaties worden bewaard als { _type, id }-stubs zodat het document opnieuw kan worden geserialiseerd naar een geldige JSON:API-aanvraag zonder datageverlies. Geef de uitvoer \"body\" van de API Call-actie door als Response-invoer.", + "Invoice Date": "Factuurdatum", + "Currency": "Valuta", + "Customer Name": "Naam klant", + "Customer": "Klant", + "Customer: Raw ID": "Klant: ruwe ID", + "Customer User": "Klantgebruiker", + "Customer User: Raw ID": "Klantgebruiker: ruwe ID", + "External Customer ID": "Externe klant-ID", + "External Customer User ID": "Externe klantgebruiker-ID", + "Total Amount": "Totaalbedrag", + "Invoice Number": "Factuurnummer", + "Title": "Titel", + "Description": "Beschrijving", + "Memo": "Notitie", + "Bill To": "Factuuradres", + "Ship To": "Verzendadres", + "Shipping Method": "Verzendmethode", + "Seller Info": "Verkoperinfo", + "External Payment URL": "Externe betalings-URL", + "Organization": "Organisatie", + "Organization: Raw ID": "Organisatie: ruwe ID", + "User": "Gebruiker", + "Owner: Raw ID": "Eigenaar: ruwe ID", + "Website": "Website", + "Website: Raw ID": "Website: ruwe ID", + "Internal Status": "Interne status", + "Internal Status: Raw ID": "Interne status: ruwe ID", + "Line Items": "Regelitems", + "Identifier": "Identificatie", + "PO Number": "Inkoopordernummer", + "Customer Notes": "Klantnotities", + "Ship Until Date": "Verzenden tot datum", + "Overridden Shipping Cost": "Overschreven verzendkosten", + "Estimated Shipping Cost": "Geschatte verzendkosten", + "Shipping Method Type": "Type verzendmethode", + "Disable Promotions": "Promoties uitschakelen", + "Payment Term": "Betalingstermijn", + "Payment Term: Raw ID": "Betalingstermijn: ruwe ID", + "Warehouse": "Magazijn", + "Warehouse: Raw ID": "Magazijn: ruwe ID", + "Parent Order": "Bovenliggende order", + "Parent Order: Raw ID": "Bovenliggende order: ruwe ID", + "Status": "Status", + "Status: Raw ID": "Status: ruwe ID", + "Billing: Label": "Facturatie: label", + "Billing: First Name": "Facturatie: voornaam", + "Billing: Last Name": "Facturatie: achternaam", + "Billing: Organization": "Facturatie: organisatie", + "Billing: Phone": "Facturatie: telefoon", + "Billing: Street": "Facturatie: straat", + "Billing: Street 2": "Facturatie: straat 2", + "Billing: City": "Facturatie: stad", + "Billing: Postal Code": "Facturatie: postcode", + "Billing: Country": "Facturatie: land", + "Billing: Country Raw ID": "Facturatie: land ruwe ID", + "Billing: Region / State": "Facturatie: regio / staat", + "Billing: Region Raw ID": "Facturatie: regio ruwe ID", + "Billing: Custom Region": "Facturatie: aangepaste regio", + "Shipping: Label": "Verzending: label", + "Shipping: First Name": "Verzending: voornaam", + "Shipping: Last Name": "Verzending: achternaam", + "Shipping: Organization": "Verzending: organisatie", + "Shipping: Phone": "Verzending: telefoon", + "Shipping: Street": "Verzending: straat", + "Shipping: Street 2": "Verzending: straat 2", + "Shipping: City": "Verzending: stad", + "Shipping: Postal Code": "Verzending: postcode", + "Shipping: Country": "Verzending: land", + "Shipping: Country Raw ID": "Verzending: land ruwe ID", + "Shipping: Region / State": "Verzending: regio / staat", + "Shipping: Region Raw ID": "Verzending: regio ruwe ID", + "Shipping: Custom Region": "Verzending: aangepaste regio", + "Product": "Product", + "Method": "Methode", + "Resource URI": "Resource-URI", + "Headers": "Headers", + "Query Parameters": "Queryparameters", + "Request Body": "Requestbody", + "Resource Type": "Resourcetype", + "Resource ID": "Resource-ID", + "Attributes": "Attributen", + "Relationships (override)": "Relaties (overschrijven)", + "Included": "Included", + "JSON:API Response": "JSON:API-respons", + "Invoice date in YYYY-MM-DD format.": "Factuurdatum in YYYY-MM-DD-indeling.", + "ISO-4217 3-letter currency code (e.g. USD, EUR).": "ISO-4217 valutacode van 3 letters (bijv. USD, EUR).", + "Name of the company being billed. Stored as a plain-text label on the invoice.": "Naam van het bedrijf dat wordt gefactureerd. Wordt als plattetekstlabel op de factuur opgeslagen.", + "Select a customer.": "Selecteer een klant.", + "Overrides the Customer dropdown. Paste an ID from a previous step.": "Overschrijft de klantkeuzelijst. Plak een ID uit een vorige stap.", + "Select a Customer User.": "Selecteer een klantgebruiker.", + "Overrides the Customer User dropdown.": "Overschrijft de klantgebruikerskeuzelijst.", + "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Een optionele ID-referentie naar een klant. Kan worden gebruikt om een willekeurige externe ID op te slaan.", + "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Een optionele ID-referentie naar een klantgebruiker. Kan worden gebruikt om een willekeurige externe ID op te slaan.", + "Total invoice amount. Should equal the sum of all line item row totals.": "Totaal factuurbedrag. Moet gelijk zijn aan de som van alle regelitemtotalen.", + "Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.": "Opeenvolgend factuurnummer (bijv. INV-2026-00001). Wordt automatisch gegenereerd als dit leeg wordt gelaten.", + "Alternative invoice title that reflects its nature.": "Alternatieve factuurtitel die de aard ervan weergeeft.", + "Internal description of the invoice.": "Interne beschrijving van de factuur.", + "Short memo visible on the invoice (e.g. \"Thank you!\").": "Korte notitie die zichtbaar is op de factuur (bijv. \"Thank you!\").", + "Billing address HTML string (e.g. 123 Main St, City, Country).": "HTML-tekenreeks voor factuuradres (bijv. 123 Main St, stad, land).", + "Shipping address HTML string.": "HTML-tekenreeks voor verzendadres.", + "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Label voor verzendmethode (bijv. \"International Shipping (Tracking #: 123)\").", + "Seller contact / address HTML string.": "HTML-tekenreeks voor verkoperscontact/adres.", + "URL for the external payment page.": "URL voor de externe betalingspagina.", + "Search organizations.": "Zoek organisaties.", + "Overrides the Organization dropdown.": "Overschrijft de organisatiekeuzelijst.", + "Search user by name, username or email.": "Zoek gebruiker op naam, gebruikersnaam of e-mailadres.", + "Overrides the Owner dropdown.": "Overschrijft de eigenaar-keuzelijst.", + "Search websites.": "Zoek websites.", + "Overrides the Website dropdown.": "Overschrijft de websitekeuzelijst.", + "Invoice internal status (e.g. Draft, Open).": "Interne factuurstatus (bijv. Draft, Open).", + "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Overschrijft de keuzelijst voor interne status (bijv. draft, open, paid).", + "Invoice line items. Each item is sent via JSON:API included.": "Factuurregelitems. Elk item wordt als JSON:API included verzonden.", + "The customer this order belongs to.": "De klant waartoe deze order behoort.", + "Unique order reference (e.g. FR1012401Z).": "Unieke orderreferentie (bijv. FR1012401Z).", + "Purchase order number provided by the buyer.": "Inkoopordernummer opgegeven door de koper.", + "Notes from the customer (e.g. \"Call before delivery\").": "Notities van de klant (bijv. \"Call before delivery\").", + "Latest acceptable ship date in YYYY-MM-DD format.": "Laatste aanvaardbare verzenddatum in YYYY-MM-DD-indeling.", + "Custom shipping cost that overrides the calculated value.": "Aangepaste verzendkosten die de berekende waarde overschrijven.", + "Shipping cost calculated from the selected shipping method.": "Verzendkosten berekend op basis van de geselecteerde verzendmethode.", + "The shipping method selected for the order (e.g. \"flat_rate_2\").": "De voor de order geselecteerde verzendmethode (bijv. \"flat_rate_2\").", + "The shipping method type (e.g. \"primary\").": "Het type verzendmethode (bijv. \"primary\").", + "Prevent the promotions engine from running for this order.": "Voorkom dat de promotie-engine wordt uitgevoerd voor deze order.", + "Order internal status (e.g. Open, Cancelled).": "Interne orderstatus (bijv. Open, Cancelled).", + "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Overschrijft de interne-statuskeuzelijst (bijv. order_internal_status.open).", + "Search payment terms.": "Zoek betalingstermijnen.", + "Overrides the Payment Term dropdown.": "Overschrijft de betalingstermijn-keuzelijst.", + "Search warehouses.": "Zoek magazijnen.", + "Overrides the Warehouse dropdown.": "Overschrijft de magazijn-keuzelijst.", + "Search orders to use as the parent order.": "Zoek orders om als bovenliggende order te gebruiken.", + "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Overschrijft de bovenliggende-order-keuzelijst. Plak een order-ID uit een vorige stap.", + "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Orderstatus beheerd door een extern systeem (alleen relevant wanneer \"Enable External Status Management\" is ingeschakeld).", + "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Overschrijft de statuskeuzelijst. Wordt alleen toegepast als \"Enable External Status Management\" is ingeschakeld.", + "Address label (e.g. \"Main Office\").": "Adreslabel (bijv. \"Main Office\").", + "ISO-3166 country. Start typing to filter the list.": "ISO-3166-land. Begin te typen om de lijst te filteren.", + "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overschrijft de landkeuzelijst voor facturatie (ISO-3166-landcode met 2 letters, bijv. US).", + "Region or state. Select a country first. Start typing to filter.": "Regio of staat. Selecteer eerst een land. Begin te typen om te filteren.", + "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overschrijft de regio-keuzelijst voor facturatie (ISO 3166-2-code, bijv. US-NY).", + "Free-text region for countries without predefined regions.": "Vrijetekstregio voor landen zonder vooraf gedefinieerde regio's.", + "Address label (e.g. \"Warehouse East\").": "Adreslabel (bijv. \"Warehouse East\").", + "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overschrijft de landkeuzelijst voor verzending (ISO-3166-landcode met 2 letters, bijv. US).", + "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overschrijft de regiokeuzelijst voor verzending (ISO 3166-2-code, bijv. US-NY).", + "Order line items.": "Orderregelitems.", + "Search products.": "Zoek producten.", + "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Pad relatief ten opzichte van de API-basis, bijv. /orders, /orders/42, /invoices?filter[status]=open", + "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Requestheaders als JSON-object. Standaardwaarden omvatten Accept en X-Include voor JSON:API.", + "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Aanvullende queryparameters als JSON-object, bijv. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", + "Request body as a JSON object (used for POST / PATCH).": "Requestbody als JSON-object (gebruikt voor POST / PATCH).", + "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Het JSON:API-resourcetype (bijv. orders, invoices, products). Het veld _type wordt automatisch als resourcetype gebruikt.", + "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID van de resource - laat leeg bij het aanmaken van een nieuw record (POST) of wanneer de invoer een id-veld bevat (het id-veld wordt automatisch opgepikt).", + "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Het vlakke object dat moet worden geserialiseerd. Geef hier het volledige JSON-document door - arrays en objecten worden automatisch in \"relationships\" geplaatst; alleen scalars worden attributen. Voor handmatige opbouw gebruikt u eenvoudige sleutel/waarde-paren, bijv. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", + "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Optionele expliciete relatietoewijzing die voorrang heeft op relaties die in Attributes worden gedetecteerd. Elke sleutel is een relatienaam; de waarde is een enkel koppelingsobject {\"type\":\"customers\",\"id\":\"42\"}, een array met koppelingsobjecten of null. Voorbeeld: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", + "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Optionele array met volledige JSON:API-resourceobjecten om op te nemen in de sectie \"included\" van het verzoek. Als de invoer van Attributes al een array \"included\" bevat (bijv. de ruwe API Call-responsbody), wordt deze automatisch doorgegeven - dit veld hoeft alleen te worden ingesteld bij het handmatig toevoegen of overschrijven van opgenomen resources. Elk item moet een geldige JSON:API-resource zijn met de velden \"type\", \"id\" en \"attributes\".", + "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "De ruwe JSON:API-responsbody - het veld \"body\" van de uitvoer van de API Call-actie. Moet een bovenliggende sleutel \"data\" bevatten (één resource of een collectie).", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "DELETE": "DELETE", + "New Order": "Nieuwe order", + "Order Removal": "Orderverwijdering", + "Order Update": "Orderupdate", + "New Invoice": "Nieuwe factuur", + "Invoice Removal": "Factuurverwijdering", + "Invoice Update": "Factuurupdate", + "Triggered when a new order is created": "Wordt geactiveerd wanneer een nieuwe order wordt aangemaakt", + "Triggered when an order is deleted": "Wordt geactiveerd wanneer een order wordt verwijderd", + "Triggered when an order is updated": "Wordt geactiveerd wanneer een order wordt bijgewerkt", + "Triggered when a new invoice is created": "Wordt geactiveerd wanneer een nieuwe factuur wordt aangemaakt", + "Triggered when an invoice is deleted": "Wordt geactiveerd wanneer een factuur wordt verwijderd", + "Triggered when an invoice is updated": "Wordt geactiveerd wanneer een factuur wordt bijgewerkt" +} + diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json new file mode 100644 index 000000000000..85e4f85b05ad --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -0,0 +1,191 @@ +{ + "B2B digital commerce solution": "Rozwiązanie handlu cyfrowego B2B", + "Server URL": "Adres URL serwera", + "Admin Prefix": "Prefiks panelu administracyjnego", + "Client ID": "Identyfikator klienta", + "Client Secret": "Sekret klienta", + "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "Bazowy adres URL Twojej instancji OroCommerce (np. https://your-store.com).", + "The admin panel URL prefix (default is \"admin\").": "Prefiks URL panelu administracyjnego (domyślnie \"admin\").", + "The OAuth Client ID from your OroCommerce OAuth application.": "Identyfikator klienta OAuth z Twojej aplikacji OAuth w OroCommerce.", + "The OAuth Client Secret from your OroCommerce OAuth application.": "Sekret klienta OAuth z Twojej aplikacji OAuth w OroCommerce.", + "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nUwierzytelnij się w API OroCommerce, używając danych klienta OAuth 2.0 (Client Credentials).\n\n**Kroki, aby uzyskać dane logowania:**\n1. Zaloguj się do panelu administracyjnego OroCommerce.\n2. Przejdź do **System** > **User Management** > **OAuth Applications**.\n3. Kliknij **Create OAuth Application** i skonfiguruj:\n - **Application Name**: Wpisz opisową nazwę (np. \"Activepieces Integration\")\n - **Grants**: Wybierz **Client Credentials**\n - **Redirect URIs**: Niewymagane dla przepływu Client Credentials\n4. Zapisz aplikację i skopiuj wartości **Client ID** oraz **Client Secret**.\n5. Zanotuj **Server URL** (np. `https://your-store.com`) oraz **Admin Prefix** (zwykle `admin`).\n ", + "Create Invoice": "Utwórz fakturę", + "Create Order": "Utwórz zamówienie", + "API Call": "Wywołanie API", + "Serialize JSON:API Request": "Serializuj żądanie JSON:API", + "Unserialize JSON:API Response": "Deserializuj odpowiedź JSON:API", + "Creates a new invoice record in OroCommerce.": "Tworzy nowy rekord faktury w OroCommerce.", + "Creates a new order record in OroCommerce.": "Tworzy nowy rekord zamówienia w OroCommerce.", + "Make a direct authenticated call to the OroCommerce JSON:API.": "Wykonuje bezpośrednie, uwierzytelnione wywołanie do OroCommerce JSON:API.", + "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Konwertuje zwykły obiekt na treść żądania zgodną z JSON:API. Pola relacji są wykrywane automatycznie — każda wartość będąca tablicą lub obiektem jest traktowana jako relacja, a tylko wartości skalarne stają się atrybutami. Wynik można przekazać bezpośrednio jako treść żądania w akcji Wywołanie API.", + "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Spłaszcza treść odpowiedzi JSON:API do zwykłego obiektu (lub tablicy obiektów). Relacje z sekcji included są rozwiązywane i wstawiane inline. Nierozwiązane odwołania do relacji są zachowywane jako zaślepki { _type, id }, aby dokument można było ponownie zserializować do poprawnego żądania JSON:API bez utraty danych. Przekaż wynik \"body\" z akcji Wywołanie API jako wejście Response.", + "Invoice Date": "Data faktury", + "Currency": "Waluta", + "Customer Name": "Nazwa klienta", + "Customer": "Klient", + "Customer: Raw ID": "Klient: surowe ID", + "Customer User": "Użytkownik klienta", + "Customer User: Raw ID": "Użytkownik klienta: surowe ID", + "External Customer ID": "Zewnętrzne ID klienta", + "External Customer User ID": "Zewnętrzne ID użytkownika klienta", + "Total Amount": "Kwota łączna", + "Invoice Number": "Numer faktury", + "Title": "Tytuł", + "Description": "Opis", + "Memo": "Notatka", + "Bill To": "Adres do faktury", + "Ship To": "Adres dostawy", + "Shipping Method": "Metoda wysyłki", + "Seller Info": "Informacje o sprzedawcy", + "External Payment URL": "Zewnętrzny URL płatności", + "Organization": "Organizacja", + "Organization: Raw ID": "Organizacja: surowe ID", + "User": "Użytkownik", + "Owner: Raw ID": "Właściciel: surowe ID", + "Website": "Strona internetowa", + "Website: Raw ID": "Strona internetowa: surowe ID", + "Internal Status": "Status wewnętrzny", + "Internal Status: Raw ID": "Status wewnętrzny: surowe ID", + "Line Items": "Pozycje", + "Identifier": "Identyfikator", + "PO Number": "Numer zamówienia zakupu (PO)", + "Customer Notes": "Uwagi klienta", + "Ship Until Date": "Data wysyłki najpóźniej do", + "Overridden Shipping Cost": "Nadpisany koszt wysyłki", + "Estimated Shipping Cost": "Szacowany koszt wysyłki", + "Shipping Method Type": "Typ metody wysyłki", + "Disable Promotions": "Wyłącz promocje", + "Payment Term": "Termin płatności", + "Payment Term: Raw ID": "Termin płatności: surowe ID", + "Warehouse": "Magazyn", + "Warehouse: Raw ID": "Magazyn: surowe ID", + "Parent Order": "Zamówienie nadrzędne", + "Parent Order: Raw ID": "Zamówienie nadrzędne: surowe ID", + "Status": "Status", + "Status: Raw ID": "Status: surowe ID", + "Billing: Label": "Fakturowanie: etykieta", + "Billing: First Name": "Fakturowanie: imię", + "Billing: Last Name": "Fakturowanie: nazwisko", + "Billing: Organization": "Fakturowanie: organizacja", + "Billing: Phone": "Fakturowanie: telefon", + "Billing: Street": "Fakturowanie: ulica", + "Billing: Street 2": "Fakturowanie: ulica 2", + "Billing: City": "Fakturowanie: miasto", + "Billing: Postal Code": "Fakturowanie: kod pocztowy", + "Billing: Country": "Fakturowanie: kraj", + "Billing: Country Raw ID": "Fakturowanie: kraj (surowe ID)", + "Billing: Region / State": "Fakturowanie: region / województwo", + "Billing: Region Raw ID": "Fakturowanie: region (surowe ID)", + "Billing: Custom Region": "Fakturowanie: własny region", + "Shipping: Label": "Wysyłka: etykieta", + "Shipping: First Name": "Wysyłka: imię", + "Shipping: Last Name": "Wysyłka: nazwisko", + "Shipping: Organization": "Wysyłka: organizacja", + "Shipping: Phone": "Wysyłka: telefon", + "Shipping: Street": "Wysyłka: ulica", + "Shipping: Street 2": "Wysyłka: ulica 2", + "Shipping: City": "Wysyłka: miasto", + "Shipping: Postal Code": "Wysyłka: kod pocztowy", + "Shipping: Country": "Wysyłka: kraj", + "Shipping: Country Raw ID": "Wysyłka: kraj (surowe ID)", + "Shipping: Region / State": "Wysyłka: region / województwo", + "Shipping: Region Raw ID": "Wysyłka: region (surowe ID)", + "Shipping: Custom Region": "Wysyłka: własny region", + "Product": "Produkt", + "Method": "Metoda", + "Resource URI": "URI zasobu", + "Headers": "Nagłówki", + "Query Parameters": "Parametry zapytania", + "Request Body": "Treść żądania", + "Resource Type": "Typ zasobu", + "Resource ID": "ID zasobu", + "Attributes": "Atrybuty", + "Relationships (override)": "Relacje (nadpisanie)", + "Included": "Included", + "JSON:API Response": "Odpowiedź JSON:API", + "Invoice date in YYYY-MM-DD format.": "Data faktury w formacie YYYY-MM-DD.", + "ISO-4217 3-letter currency code (e.g. USD, EUR).": "3-literowy kod waluty ISO-4217 (np. USD, EUR).", + "Name of the company being billed. Stored as a plain-text label on the invoice.": "Nazwa firmy, na którą wystawiana jest faktura. Zapisywana jako etykieta w postaci zwykłego tekstu na fakturze.", + "Select a customer.": "Wybierz klienta.", + "Overrides the Customer dropdown. Paste an ID from a previous step.": "Nadpisuje listę rozwijaną Klient. Wklej ID z poprzedniego kroku.", + "Select a Customer User.": "Wybierz użytkownika klienta.", + "Overrides the Customer User dropdown.": "Nadpisuje listę rozwijaną Użytkownik klienta.", + "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Opcjonalne odwołanie ID do klienta. Może służyć do przechowywania dowolnego zewnętrznego identyfikatora.", + "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Opcjonalne odwołanie ID do użytkownika klienta. Może służyć do przechowywania dowolnego zewnętrznego identyfikatora.", + "Total invoice amount. Should equal the sum of all line item row totals.": "Łączna kwota faktury. Powinna być równa sumie wartości łącznych wszystkich pozycji.", + "Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.": "Kolejny numer faktury (np. INV-2026-00001). Zostanie wygenerowany automatycznie, jeśli pole pozostanie puste.", + "Alternative invoice title that reflects its nature.": "Alternatywny tytuł faktury odzwierciedlający jej charakter.", + "Internal description of the invoice.": "Wewnętrzny opis faktury.", + "Short memo visible on the invoice (e.g. \"Thank you!\").": "Krótka notatka widoczna na fakturze (np. \"Thank you!\").", + "Billing address HTML string (e.g. 123 Main St, City, Country).": "Ciąg HTML adresu rozliczeniowego (np. 123 Main St, Miasto, Kraj).", + "Shipping address HTML string.": "Ciąg HTML adresu dostawy.", + "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Etykieta metody wysyłki (np. \"International Shipping (Tracking #: 123)\").", + "Seller contact / address HTML string.": "Ciąg HTML kontaktu/adresu sprzedawcy.", + "URL for the external payment page.": "Adres URL zewnętrznej strony płatności.", + "Search organizations.": "Wyszukaj organizacje.", + "Overrides the Organization dropdown.": "Nadpisuje listę rozwijaną Organizacja.", + "Search user by name, username or email.": "Wyszukaj użytkownika po imieniu, nazwie użytkownika lub adresie e-mail.", + "Overrides the Owner dropdown.": "Nadpisuje listę rozwijaną Właściciel.", + "Search websites.": "Wyszukaj strony internetowe.", + "Overrides the Website dropdown.": "Nadpisuje listę rozwijaną Strona internetowa.", + "Invoice internal status (e.g. Draft, Open).": "Wewnętrzny status faktury (np. Draft, Open).", + "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Nadpisuje listę rozwijaną Status wewnętrzny (np. draft, open, paid).", + "Invoice line items. Each item is sent via JSON:API included.": "Pozycje faktury. Każda pozycja jest wysyłana jako JSON:API included.", + "The customer this order belongs to.": "Klient, do którego należy to zamówienie.", + "Unique order reference (e.g. FR1012401Z).": "Unikalny identyfikator zamówienia (np. FR1012401Z).", + "Purchase order number provided by the buyer.": "Numer zamówienia zakupu podany przez kupującego.", + "Notes from the customer (e.g. \"Call before delivery\").": "Uwagi od klienta (np. \"Call before delivery\").", + "Latest acceptable ship date in YYYY-MM-DD format.": "Najpóźniejsza akceptowalna data wysyłki w formacie YYYY-MM-DD.", + "Custom shipping cost that overrides the calculated value.": "Niestandardowy koszt wysyłki, który nadpisuje wartość wyliczoną.", + "Shipping cost calculated from the selected shipping method.": "Koszt wysyłki wyliczony na podstawie wybranej metody wysyłki.", + "The shipping method selected for the order (e.g. \"flat_rate_2\").": "Metoda wysyłki wybrana dla zamówienia (np. \"flat_rate_2\").", + "The shipping method type (e.g. \"primary\").": "Typ metody wysyłki (np. \"primary\").", + "Prevent the promotions engine from running for this order.": "Zablokuj uruchamianie silnika promocji dla tego zamówienia.", + "Order internal status (e.g. Open, Cancelled).": "Wewnętrzny status zamówienia (np. Open, Cancelled).", + "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Nadpisuje listę rozwijaną Status wewnętrzny (np. order_internal_status.open).", + "Search payment terms.": "Wyszukaj terminy płatności.", + "Overrides the Payment Term dropdown.": "Nadpisuje listę rozwijaną Termin płatności.", + "Search warehouses.": "Wyszukaj magazyny.", + "Overrides the Warehouse dropdown.": "Nadpisuje listę rozwijaną Magazyn.", + "Search orders to use as the parent order.": "Wyszukaj zamówienia do użycia jako zamówienie nadrzędne.", + "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Nadpisuje listę rozwijaną Zamówienie nadrzędne. Wklej ID zamówienia z poprzedniego kroku.", + "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Status zamówienia zarządzany przez system zewnętrzny (istotne tylko, gdy opcja \"Enable External Status Management\" jest włączona).", + "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Nadpisuje listę rozwijaną Status. Stosowane tylko, gdy opcja \"Enable External Status Management\" jest włączona.", + "Address label (e.g. \"Main Office\").": "Etykieta adresu (np. \"Main Office\").", + "ISO-3166 country. Start typing to filter the list.": "Kraj ISO-3166. Zacznij pisać, aby filtrować listę.", + "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Nadpisuje listę rozwijaną Kraj rozliczeniowy (dwuliterowy kod ISO-3166, np. US).", + "Region or state. Select a country first. Start typing to filter.": "Region lub województwo. Najpierw wybierz kraj. Zacznij pisać, aby filtrować.", + "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Nadpisuje listę rozwijaną Region rozliczeniowy (kod ISO 3166-2, np. US-NY).", + "Free-text region for countries without predefined regions.": "Region wprowadzany ręcznie dla krajów bez zdefiniowanych regionów.", + "Address label (e.g. \"Warehouse East\").": "Etykieta adresu (np. \"Warehouse East\").", + "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Nadpisuje listę rozwijaną Kraj dostawy (dwuliterowy kod ISO-3166, np. US).", + "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Nadpisuje listę rozwijaną Region dostawy (kod ISO 3166-2, np. US-NY).", + "Order line items.": "Pozycje zamówienia.", + "Search products.": "Wyszukaj produkty.", + "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Ścieżka względna względem bazowego adresu API, np. /orders, /orders/42, /invoices?filter[status]=open", + "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Nagłówki żądania jako obiekt JSON. Domyślnie zawiera Accept oraz X-Include dla JSON:API.", + "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Dodatkowe parametry zapytania jako obiekt JSON, np. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", + "Request body as a JSON object (used for POST / PATCH).": "Treść żądania jako obiekt JSON (używane dla POST / PATCH).", + "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Typ zasobu JSON:API (np. orders, invoices, products). Pole _type jest automatycznie używane jako typ zasobu.", + "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID zasobu — pozostaw puste podczas tworzenia nowego rekordu (POST) lub gdy dane wejściowe zawierają pole id (pole id zostanie wykryte automatycznie).", + "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Spłaszczony obiekt do serializacji. Przekaż tutaj pełny dokument JSON — tablice i obiekty są automatycznie umieszczane w \"relationships\"; tylko wartości skalarne stają się atrybutami. Do ręcznej konstrukcji użyj prostych par klucz/wartość, np. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", + "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Opcjonalna jawna mapa relacji, która ma pierwszeństwo przed relacjami wykrytymi w Attributes. Każdy klucz to nazwa relacji; wartość to pojedynczy obiekt powiązania {\"type\":\"customers\",\"id\":\"42\"}, tablica obiektów powiązania lub null. Przykład: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", + "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Opcjonalna tablica pełnych obiektów zasobów JSON:API do osadzenia w sekcji \"included\" żądania. Jeśli dane wejściowe Attributes już zawierają tablicę \"included\" (np. surową treść odpowiedzi z Wywołania API), zostanie ona przekazana automatycznie — to pole należy ustawić tylko przy ręcznym dodawaniu lub nadpisywaniu zasobów included. Każdy element musi być poprawnym zasobem JSON:API z polami \"type\", \"id\" oraz \"attributes\".", + "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Surowa treść odpowiedzi JSON:API — pole \"body\" z wyniku akcji Wywołanie API. Musi zawierać klucz najwyższego poziomu \"data\" (pojedynczy zasób lub kolekcję).", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "DELETE": "DELETE", + "New Order": "Nowe zamówienie", + "Order Removal": "Usunięcie zamówienia", + "Order Update": "Aktualizacja zamówienia", + "New Invoice": "Nowa faktura", + "Invoice Removal": "Usunięcie faktury", + "Invoice Update": "Aktualizacja faktury", + "Triggered when a new order is created": "Wyzwalane, gdy zostanie utworzone nowe zamówienie", + "Triggered when an order is deleted": "Wyzwalane, gdy zamówienie zostanie usunięte", + "Triggered when an order is updated": "Wyzwalane, gdy zamówienie zostanie zaktualizowane", + "Triggered when a new invoice is created": "Wyzwalane, gdy zostanie utworzona nowa faktura", + "Triggered when an invoice is deleted": "Wyzwalane, gdy faktura zostanie usunięta", + "Triggered when an invoice is updated": "Wyzwalane, gdy faktura zostanie zaktualizowana" +} diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json new file mode 100644 index 000000000000..d1dc1cad6504 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -0,0 +1,191 @@ +{ + "B2B digital commerce solution": "B2B digital commerce solution", + "Server URL": "Server URL", + "Admin Prefix": "Admin Prefix", + "Client ID": "Client ID", + "Client Secret": "Client Secret", + "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "The base URL of your OroCommerce instance (e.g., https://your-store.com).", + "The admin panel URL prefix (default is \"admin\").": "The admin panel URL prefix (default is \"admin\").", + "The OAuth Client ID from your OroCommerce OAuth application.": "The OAuth Client ID from your OroCommerce OAuth application.", + "The OAuth Client Secret from your OroCommerce OAuth application.": "The OAuth Client Secret from your OroCommerce OAuth application.", + "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **Client ID** and **Client Secret**.\n5. Note your **Server URL** (e.g., `https://your-store.com`) and **Admin Prefix** (usually `admin`).\n ", + "Create Invoice": "Create Invoice", + "Create Order": "Create Order", + "API Call": "API Call", + "Serialize JSON:API Request": "Serialize JSON:API Request", + "Unserialize JSON:API Response": "Unserialize JSON:API Response", + "Creates a new invoice record in OroCommerce.": "Creates a new invoice record in OroCommerce.", + "Creates a new order record in OroCommerce.": "Creates a new order record in OroCommerce.", + "Make a direct authenticated call to the OroCommerce JSON:API.": "Make a direct authenticated call to the OroCommerce JSON:API.", + "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.", + "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.", + "Invoice Date": "Invoice Date", + "Currency": "Currency", + "Customer Name": "Customer Name", + "Customer": "Customer", + "Customer: Raw ID": "Customer: Raw ID", + "Customer User": "Customer User", + "Customer User: Raw ID": "Customer User: Raw ID", + "External Customer ID": "External Customer ID", + "External Customer User ID": "External Customer User ID", + "Total Amount": "Total Amount", + "Invoice Number": "Invoice Number", + "Title": "Title", + "Description": "Description", + "Memo": "Memo", + "Bill To": "Bill To", + "Ship To": "Ship To", + "Shipping Method": "Shipping Method", + "Seller Info": "Seller Info", + "External Payment URL": "External Payment URL", + "Organization": "Organization", + "Organization: Raw ID": "Organization: Raw ID", + "User": "User", + "Owner: Raw ID": "Owner: Raw ID", + "Website": "Website", + "Website: Raw ID": "Website: Raw ID", + "Internal Status": "Internal Status", + "Internal Status: Raw ID": "Internal Status: Raw ID", + "Line Items": "Line Items", + "Identifier": "Identifier", + "PO Number": "PO Number", + "Customer Notes": "Customer Notes", + "Ship Until Date": "Ship Until Date", + "Overridden Shipping Cost": "Overridden Shipping Cost", + "Estimated Shipping Cost": "Estimated Shipping Cost", + "Shipping Method Type": "Shipping Method Type", + "Disable Promotions": "Disable Promotions", + "Payment Term": "Payment Term", + "Payment Term: Raw ID": "Payment Term: Raw ID", + "Warehouse": "Warehouse", + "Warehouse: Raw ID": "Warehouse: Raw ID", + "Parent Order": "Parent Order", + "Parent Order: Raw ID": "Parent Order: Raw ID", + "Status": "Status", + "Status: Raw ID": "Status: Raw ID", + "Billing: Label": "Billing: Label", + "Billing: First Name": "Billing: First Name", + "Billing: Last Name": "Billing: Last Name", + "Billing: Organization": "Billing: Organization", + "Billing: Phone": "Billing: Phone", + "Billing: Street": "Billing: Street", + "Billing: Street 2": "Billing: Street 2", + "Billing: City": "Billing: City", + "Billing: Postal Code": "Billing: Postal Code", + "Billing: Country": "Billing: Country", + "Billing: Country Raw ID": "Billing: Country Raw ID", + "Billing: Region / State": "Billing: Region / State", + "Billing: Region Raw ID": "Billing: Region Raw ID", + "Billing: Custom Region": "Billing: Custom Region", + "Shipping: Label": "Shipping: Label", + "Shipping: First Name": "Shipping: First Name", + "Shipping: Last Name": "Shipping: Last Name", + "Shipping: Organization": "Shipping: Organization", + "Shipping: Phone": "Shipping: Phone", + "Shipping: Street": "Shipping: Street", + "Shipping: Street 2": "Shipping: Street 2", + "Shipping: City": "Shipping: City", + "Shipping: Postal Code": "Shipping: Postal Code", + "Shipping: Country": "Shipping: Country", + "Shipping: Country Raw ID": "Shipping: Country Raw ID", + "Shipping: Region / State": "Shipping: Region / State", + "Shipping: Region Raw ID": "Shipping: Region Raw ID", + "Shipping: Custom Region": "Shipping: Custom Region", + "Product": "Product", + "Method": "Method", + "Resource URI": "Resource URI", + "Headers": "Headers", + "Query Parameters": "Query Parameters", + "Request Body": "Request Body", + "Resource Type": "Resource Type", + "Resource ID": "Resource ID", + "Attributes": "Attributes", + "Relationships (override)": "Relationships (override)", + "Included": "Included", + "JSON:API Response": "JSON:API Response", + "Invoice date in YYYY-MM-DD format.": "Invoice date in YYYY-MM-DD format.", + "ISO-4217 3-letter currency code (e.g. USD, EUR).": "ISO-4217 3-letter currency code (e.g. USD, EUR).", + "Name of the company being billed. Stored as a plain-text label on the invoice.": "Name of the company being billed. Stored as a plain-text label on the invoice.", + "Select a customer.": "Select a customer.", + "Overrides the Customer dropdown. Paste an ID from a previous step.": "Overrides the Customer dropdown. Paste an ID from a previous step.", + "Select a Customer User.": "Select a Customer User.", + "Overrides the Customer User dropdown.": "Overrides the Customer User dropdown.", + "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.", + "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.", + "Total invoice amount. Should equal the sum of all line item row totals.": "Total invoice amount. Should equal the sum of all line item row totals.", + "Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.": "Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.", + "Alternative invoice title that reflects its nature.": "Alternative invoice title that reflects its nature.", + "Internal description of the invoice.": "Internal description of the invoice.", + "Short memo visible on the invoice (e.g. \"Thank you!\").": "Short memo visible on the invoice (e.g. \"Thank you!\").", + "Billing address HTML string (e.g. 123 Main St, City, Country).": "Billing address HTML string (e.g. 123 Main St, City, Country).", + "Shipping address HTML string.": "Shipping address HTML string.", + "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").", + "Seller contact / address HTML string.": "Seller contact / address HTML string.", + "URL for the external payment page.": "URL for the external payment page.", + "Search organizations.": "Search organizations.", + "Overrides the Organization dropdown.": "Overrides the Organization dropdown.", + "Search user by name, username or email.": "Search user by name, username or email.", + "Overrides the Owner dropdown.": "Overrides the Owner dropdown.", + "Search websites.": "Search websites.", + "Overrides the Website dropdown.": "Overrides the Website dropdown.", + "Invoice internal status (e.g. Draft, Open).": "Invoice internal status (e.g. Draft, Open).", + "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Overrides the Internal Status dropdown (e.g. draft, open, paid).", + "Invoice line items. Each item is sent via JSON:API included.": "Invoice line items. Each item is sent via JSON:API included.", + "The customer this order belongs to.": "The customer this order belongs to.", + "Unique order reference (e.g. FR1012401Z).": "Unique order reference (e.g. FR1012401Z).", + "Purchase order number provided by the buyer.": "Purchase order number provided by the buyer.", + "Notes from the customer (e.g. \"Call before delivery\").": "Notes from the customer (e.g. \"Call before delivery\").", + "Latest acceptable ship date in YYYY-MM-DD format.": "Latest acceptable ship date in YYYY-MM-DD format.", + "Custom shipping cost that overrides the calculated value.": "Custom shipping cost that overrides the calculated value.", + "Shipping cost calculated from the selected shipping method.": "Shipping cost calculated from the selected shipping method.", + "The shipping method selected for the order (e.g. \"flat_rate_2\").": "The shipping method selected for the order (e.g. \"flat_rate_2\").", + "The shipping method type (e.g. \"primary\").": "The shipping method type (e.g. \"primary\").", + "Prevent the promotions engine from running for this order.": "Prevent the promotions engine from running for this order.", + "Order internal status (e.g. Open, Cancelled).": "Order internal status (e.g. Open, Cancelled).", + "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Overrides the Internal Status dropdown (e.g. order_internal_status.open).", + "Search payment terms.": "Search payment terms.", + "Overrides the Payment Term dropdown.": "Overrides the Payment Term dropdown.", + "Search warehouses.": "Search warehouses.", + "Overrides the Warehouse dropdown.": "Overrides the Warehouse dropdown.", + "Search orders to use as the parent order.": "Search orders to use as the parent order.", + "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Overrides the Parent Order dropdown. Paste an order ID from a previous step.", + "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).", + "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.", + "Address label (e.g. \"Main Office\").": "Address label (e.g. \"Main Office\").", + "ISO-3166 country. Start typing to filter the list.": "ISO-3166 country. Start typing to filter the list.", + "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).", + "Region or state. Select a country first. Start typing to filter.": "Region or state. Select a country first. Start typing to filter.", + "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).", + "Free-text region for countries without predefined regions.": "Free-text region for countries without predefined regions.", + "Address label (e.g. \"Warehouse East\").": "Address label (e.g. \"Warehouse East\").", + "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).", + "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).", + "Order line items.": "Order line items.", + "Search products.": "Search products.", + "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open", + "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.", + "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", + "Request body as a JSON object (used for POST / PATCH).": "Request body as a JSON object (used for POST / PATCH).", + "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.", + "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).", + "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", + "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", + "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.", + "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "DELETE": "DELETE", + "New Order": "New Order", + "Order Removal": "Order Removal", + "Order Update": "Order Update", + "New Invoice": "New Invoice", + "Invoice Removal": "Invoice Removal", + "Invoice Update": "Invoice Update", + "Triggered when a new order is created": "Triggered when a new order is created", + "Triggered when an order is deleted": "Triggered when an order is deleted", + "Triggered when an order is updated": "Triggered when an order is updated", + "Triggered when a new invoice is created": "Triggered when a new invoice is created", + "Triggered when an invoice is deleted": "Triggered when an invoice is deleted", + "Triggered when an invoice is updated": "Triggered when an invoice is updated" +} \ No newline at end of file diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json new file mode 100644 index 000000000000..5ddb2d9f5103 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -0,0 +1,191 @@ +{ + "B2B digital commerce solution": "B2B цифрове рішення для електронної комерції", + "Server URL": "URL сервера", + "Admin Prefix": "Префікс адмін-панелі", + "Client ID": "Ідентифікатор клієнта (Client ID)", + "Client Secret": "Секрет клієнта (Client Secret)", + "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "Базова URL-адреса вашого інстансу OroCommerce (наприклад, https://your-store.com).", + "The admin panel URL prefix (default is \"admin\").": "Префікс URL адмін-панелі (за замовчуванням \"admin\").", + "The OAuth Client ID from your OroCommerce OAuth application.": "OAuth Client ID з вашого OAuth-застосунку OroCommerce.", + "The OAuth Client Secret from your OroCommerce OAuth application.": "OAuth Client Secret з вашого OAuth-застосунку OroCommerce.", + "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nАвтентифікуйтеся в API OroCommerce за допомогою OAuth 2.0 Client Credentials.\n\n**Кроки для отримання облікових даних:**\n1. Увійдіть в адмін-панель OroCommerce.\n2. Перейдіть до **System** > **User Management** > **OAuth Applications**.\n3. Натисніть **Create OAuth Application** та налаштуйте:\n - **Application Name**: Вкажіть описову назву (наприклад, \"Activepieces Integration\")\n - **Grants**: Оберіть **Client Credentials**\n - **Redirect URIs**: Не потрібно для потоку Client Credentials\n4. Збережіть застосунок і скопіюйте **Client ID** та **Client Secret**.\n5. Запам’ятайте ваш **Server URL** (наприклад, `https://your-store.com`) та **Admin Prefix** (зазвичай `admin`).\n ", + "Create Invoice": "Створити рахунок-фактуру", + "Create Order": "Створити замовлення", + "API Call": "API-запит", + "Serialize JSON:API Request": "Серіалізувати запит JSON:API", + "Unserialize JSON:API Response": "Десеріалізувати відповідь JSON:API", + "Creates a new invoice record in OroCommerce.": "Створює новий запис рахунку-фактури в OroCommerce.", + "Creates a new order record in OroCommerce.": "Створює новий запис замовлення в OroCommerce.", + "Make a direct authenticated call to the OroCommerce JSON:API.": "Виконує прямий автентифікований виклик до OroCommerce JSON:API.", + "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Перетворює простий об’єкт у тіло запиту, сумісне з JSON:API. Поля зв’язків визначаються автоматично — будь-яке значення типу масив або об’єкт розглядається як relationship, а лише скалярні значення стають attributes. Результат можна напряму передати в поле Request Body дії API Call.", + "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Перетворює тіло відповіді JSON:API у простий об’єкт (або масив об’єктів). Зв’язки з included розв’язуються та вбудовуються. Нерозв’язані посилання на зв’язки зберігаються як заглушки { _type, id }, щоб документ можна було повторно серіалізувати у коректний запит JSON:API без втрати даних. Передайте вихід \"body\" з дії API Call у вхід Response.", + "Invoice Date": "Дата рахунку", + "Currency": "Валюта", + "Customer Name": "Назва клієнта", + "Customer": "Клієнт", + "Customer: Raw ID": "Клієнт: сирий ID", + "Customer User": "Користувач клієнта", + "Customer User: Raw ID": "Користувач клієнта: сирий ID", + "External Customer ID": "Зовнішній ID клієнта", + "External Customer User ID": "Зовнішній ID користувача клієнта", + "Total Amount": "Загальна сума", + "Invoice Number": "Номер рахунку", + "Title": "Назва", + "Description": "Опис", + "Memo": "Примітка", + "Bill To": "Платник", + "Ship To": "Адреса доставки", + "Shipping Method": "Спосіб доставки", + "Seller Info": "Інформація про продавця", + "External Payment URL": "Зовнішній URL оплати", + "Organization": "Організація", + "Organization: Raw ID": "Організація: сирий ID", + "User": "Користувач", + "Owner: Raw ID": "Власник: сирий ID", + "Website": "Вебсайт", + "Website: Raw ID": "Вебсайт: сирий ID", + "Internal Status": "Внутрішній статус", + "Internal Status: Raw ID": "Внутрішній статус: сирий ID", + "Line Items": "Позиції", + "Identifier": "Ідентифікатор", + "PO Number": "Номер PO", + "Customer Notes": "Примітки клієнта", + "Ship Until Date": "Відправити до дати", + "Overridden Shipping Cost": "Перевизначена вартість доставки", + "Estimated Shipping Cost": "Орієнтовна вартість доставки", + "Shipping Method Type": "Тип способу доставки", + "Disable Promotions": "Вимкнути промоакції", + "Payment Term": "Умови оплати", + "Payment Term: Raw ID": "Умови оплати: сирий ID", + "Warehouse": "Склад", + "Warehouse: Raw ID": "Склад: сирий ID", + "Parent Order": "Батьківське замовлення", + "Parent Order: Raw ID": "Батьківське замовлення: сирий ID", + "Status": "Статус", + "Status: Raw ID": "Статус: сирий ID", + "Billing: Label": "Платіжна адреса: мітка", + "Billing: First Name": "Платіжна адреса: ім’я", + "Billing: Last Name": "Платіжна адреса: прізвище", + "Billing: Organization": "Платіжна адреса: організація", + "Billing: Phone": "Платіжна адреса: телефон", + "Billing: Street": "Платіжна адреса: вулиця", + "Billing: Street 2": "Платіжна адреса: вулиця 2", + "Billing: City": "Платіжна адреса: місто", + "Billing: Postal Code": "Платіжна адреса: поштовий індекс", + "Billing: Country": "Платіжна адреса: країна", + "Billing: Country Raw ID": "Платіжна адреса: країна (сирий ID)", + "Billing: Region / State": "Платіжна адреса: регіон / штат", + "Billing: Region Raw ID": "Платіжна адреса: регіон (сирий ID)", + "Billing: Custom Region": "Платіжна адреса: інший регіон", + "Shipping: Label": "Адреса доставки: мітка", + "Shipping: First Name": "Адреса доставки: ім’я", + "Shipping: Last Name": "Адреса доставки: прізвище", + "Shipping: Organization": "Адреса доставки: організація", + "Shipping: Phone": "Адреса доставки: телефон", + "Shipping: Street": "Адреса доставки: вулиця", + "Shipping: Street 2": "Адреса доставки: вулиця 2", + "Shipping: City": "Адреса доставки: місто", + "Shipping: Postal Code": "Адреса доставки: поштовий індекс", + "Shipping: Country": "Адреса доставки: країна", + "Shipping: Country Raw ID": "Адреса доставки: країна (сирий ID)", + "Shipping: Region / State": "Адреса доставки: регіон / штат", + "Shipping: Region Raw ID": "Адреса доставки: регіон (сирий ID)", + "Shipping: Custom Region": "Адреса доставки: інший регіон", + "Product": "Товар", + "Method": "Метод", + "Resource URI": "URI ресурсу", + "Headers": "Заголовки", + "Query Parameters": "Параметри запиту", + "Request Body": "Тіло запиту", + "Resource Type": "Тип ресурсу", + "Resource ID": "ID ресурсу", + "Attributes": "Атрибути", + "Relationships (override)": "Зв’язки (перевизначення)", + "Included": "Included", + "JSON:API Response": "Відповідь JSON:API", + "Invoice date in YYYY-MM-DD format.": "Дата рахунку у форматі YYYY-MM-DD.", + "ISO-4217 3-letter currency code (e.g. USD, EUR).": "3-літерний код валюти ISO-4217 (наприклад, USD, EUR).", + "Name of the company being billed. Stored as a plain-text label on the invoice.": "Назва компанії, якій виставляється рахунок. Зберігається як звичайна текстова мітка в рахунку.", + "Select a customer.": "Оберіть клієнта.", + "Overrides the Customer dropdown. Paste an ID from a previous step.": "Перевизначає випадаючий список Customer. Вставте ID з попереднього кроку.", + "Select a Customer User.": "Оберіть користувача клієнта.", + "Overrides the Customer User dropdown.": "Перевизначає випадаючий список Customer User.", + "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Необов’язкове посилання-ID на клієнта. Можна використати для збереження довільного зовнішнього ID.", + "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Необов’язкове посилання-ID на користувача клієнта. Можна використати для збереження довільного зовнішнього ID.", + "Total invoice amount. Should equal the sum of all line item row totals.": "Загальна сума рахунку. Має дорівнювати сумі всіх підсумків по позиціях.", + "Sequential invoice number (e.g. INV-2026-00001). Auto-generated if left empty.": "Послідовний номер рахунку (наприклад, INV-2026-00001). Якщо залишити порожнім — буде згенеровано автоматично.", + "Alternative invoice title that reflects its nature.": "Альтернативна назва рахунку, що відображає його характер.", + "Internal description of the invoice.": "Внутрішній опис рахунку.", + "Short memo visible on the invoice (e.g. \"Thank you!\").": "Коротка примітка, видима в рахунку (наприклад, \"Дякуємо!\").", + "Billing address HTML string (e.g. 123 Main St, City, Country).": "HTML-рядок платіжної адреси (наприклад, 123 Main St, City, Country).", + "Shipping address HTML string.": "HTML-рядок адреси доставки.", + "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Мітка способу доставки (наприклад, \"International Shipping (Tracking #: 123)\").", + "Seller contact / address HTML string.": "HTML-рядок контактів / адреси продавця.", + "URL for the external payment page.": "URL зовнішньої сторінки оплати.", + "Search organizations.": "Пошук організацій.", + "Overrides the Organization dropdown.": "Перевизначає випадаючий список Organization.", + "Search user by name, username or email.": "Пошук користувача за ім’ям, ім’ям користувача або email.", + "Overrides the Owner dropdown.": "Перевизначає випадаючий список Owner.", + "Search websites.": "Пошук вебсайтів.", + "Overrides the Website dropdown.": "Перевизначає випадаючий список Website.", + "Invoice internal status (e.g. Draft, Open).": "Внутрішній статус рахунку (наприклад, Draft, Open).", + "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Перевизначає випадаючий список Internal Status (наприклад, draft, open, paid).", + "Invoice line items. Each item is sent via JSON:API included.": "Позиції рахунку. Кожна позиція надсилається через JSON:API included.", + "The customer this order belongs to.": "Клієнт, якому належить це замовлення.", + "Unique order reference (e.g. FR1012401Z).": "Унікальний ідентифікатор замовлення (наприклад, FR1012401Z).", + "Purchase order number provided by the buyer.": "Номер замовлення на закупівлю (PO), наданий покупцем.", + "Notes from the customer (e.g. \"Call before delivery\").": "Примітки від клієнта (наприклад, \"Подзвоніть перед доставкою\").", + "Latest acceptable ship date in YYYY-MM-DD format.": "Остання прийнятна дата відправлення у форматі YYYY-MM-DD.", + "Custom shipping cost that overrides the calculated value.": "Користувацька вартість доставки, що перевизначає розраховане значення.", + "Shipping cost calculated from the selected shipping method.": "Вартість доставки, розрахована на основі обраного способу доставки.", + "The shipping method selected for the order (e.g. \"flat_rate_2\").": "Спосіб доставки, обраний для замовлення (наприклад, \"flat_rate_2\").", + "The shipping method type (e.g. \"primary\").": "Тип способу доставки (наприклад, \"primary\").", + "Prevent the promotions engine from running for this order.": "Запобігти запуску механізму промоакцій для цього замовлення.", + "Order internal status (e.g. Open, Cancelled).": "Внутрішній статус замовлення (наприклад, Open, Cancelled).", + "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Перевизначає випадаючий список Internal Status (наприклад, order_internal_status.open).", + "Search payment terms.": "Пошук умов оплати.", + "Overrides the Payment Term dropdown.": "Перевизначає випадаючий список Payment Term.", + "Search warehouses.": "Пошук складів.", + "Overrides the Warehouse dropdown.": "Перевизначає випадаючий список Warehouse.", + "Search orders to use as the parent order.": "Пошук замовлень для використання як батьківського замовлення.", + "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Перевизначає випадаючий список Parent Order. Вставте ID замовлення з попереднього кроку.", + "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Статус замовлення, яким керує зовнішня система (актуально лише коли ввімкнено \"Enable External Status Management\").", + "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Перевизначає випадаючий список Status. Застосовується лише коли ввімкнено \"Enable External Status Management\".", + "Address label (e.g. \"Main Office\").": "Мітка адреси (наприклад, \"Main Office\").", + "ISO-3166 country. Start typing to filter the list.": "Країна за ISO-3166. Почніть вводити, щоб відфільтрувати список.", + "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Перевизначає випадаючий список Billing Country (2-літерний код ISO-3166, наприклад US).", + "Region or state. Select a country first. Start typing to filter.": "Регіон або штат. Спочатку оберіть країну. Почніть вводити, щоб відфільтрувати.", + "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Перевизначає випадаючий список Billing Region (код ISO 3166-2, наприклад US-NY).", + "Free-text region for countries without predefined regions.": "Регіон у довільному форматі для країн без попередньо визначених регіонів.", + "Address label (e.g. \"Warehouse East\").": "Мітка адреси (наприклад, \"Warehouse East\").", + "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Перевизначає випадаючий список Shipping Country (2-літерний код ISO-3166, наприклад US).", + "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Перевизначає випадаючий список Shipping Region (код ISO 3166-2, наприклад US-NY).", + "Order line items.": "Позиції замовлення.", + "Search products.": "Пошук товарів.", + "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Шлях відносно бази API, наприклад /orders, /orders/42, /invoices?filter[status]=open", + "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Заголовки запиту у вигляді JSON-об’єкта. Типові значення включають Accept та X-Include для JSON:API.", + "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Додаткові параметри запиту у вигляді JSON-об’єкта, наприклад {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", + "Request body as a JSON object (used for POST / PATCH).": "Тіло запиту у вигляді JSON-об’єкта (використовується для POST / PATCH).", + "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Тип ресурсу JSON:API (наприклад, orders, invoices, products). Поле _type автоматично визначається як тип ресурсу.", + "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID ресурсу — залиште порожнім під час створення нового запису (POST) або коли вхідні дані містять поле id (це поле буде зчитано автоматично).", + "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Плоский об’єкт для серіалізації. Передайте сюди повний JSON-документ — масиви та об’єкти автоматично потрапляють у \"relationships\"; лише скалярні значення стають attributes. Для ручного складання використовуйте прості пари ключ/значення, наприклад {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", + "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Необов’язкова явна мапа relationships, яка має пріоритет над будь-якими зв’язками, визначеними всередині Attributes. Кожен ключ — назва relationship; значення — один об’єкт зв’язку {\"type\":\"customers\",\"id\":\"42\"}, масив таких об’єктів або null. Приклад: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", + "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Необов’язковий масив повних ресурсів JSON:API для вбудовування в розділ \"included\" запиту. Якщо вхідні дані Attributes вже містять масив \"included\" (наприклад, сире тіло відповіді API Call), він буде переданий автоматично — це поле потрібно лише для ручного додавання або перевизначення included-ресурсів. Кожен елемент має бути коректним ресурсом JSON:API з полями \"type\", \"id\" та \"attributes\".", + "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Сире тіло відповіді JSON:API — поле \"body\" з виходу дії API Call. Має містити ключ верхнього рівня \"data\" (один ресурс або колекція).", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "DELETE": "DELETE", + "New Order": "Нове замовлення", + "Order Removal": "Видалення замовлення", + "Order Update": "Оновлення замовлення", + "New Invoice": "Новий рахунок-фактура", + "Invoice Removal": "Видалення рахунку-фактури", + "Invoice Update": "Оновлення рахунку-фактури", + "Triggered when a new order is created": "Спрацьовує, коли створюється нове замовлення", + "Triggered when an order is deleted": "Спрацьовує, коли замовлення видалено", + "Triggered when an order is updated": "Спрацьовує, коли замовлення оновлено", + "Triggered when a new invoice is created": "Спрацьовує, коли створюється новий рахунок-фактура", + "Triggered when an invoice is deleted": "Спрацьовує, коли рахунок-фактуру видалено", + "Triggered when an invoice is updated": "Спрацьовує, коли рахунок-фактуру оновлено" +} diff --git a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts index baf68fb3414f..53707e6844b8 100644 --- a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts @@ -1,114 +1,117 @@ const invoiceData = { - type: 'invoices', - id: '1', - attributes: { - uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', - invoiceNumber: 'INV-2026-02-00001', - invoiceDate: '2026-02-03', - currency: 'USD', - totalAmount: '59.2100', - customerName: 'Customer A', - refCustomerId: null, - refCustomerUserId: null, - title: 'Sample Invoice', - description: 'Sample prepaid service', - memo: 'Thank you for choosing us!', - billTo: '101 Pine St, Ogdenville, USA, 44556', - shipTo: '654 Elm St, Shelbyville, USA, 09876', - shippingMethod: 'International Shipping (Tracking #: 901234)', - sellerInfo: - 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', - externalPaymentUrl: 'https://example.com/pay/123456790', - createdAt: '2026-02-03T11:33:49Z', - updatedAt: '2026-02-18T11:35:37Z', - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: 0, - timesContactedIn: 0, - timesContactedOut: 0, - paymentStatus: { - code: 'refunded', - label: 'Refunded', - isForced: false, + data: { + type: 'invoices', + id: '1', + attributes: { + uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', + invoiceNumber: 'INV-2026-02-00001', + invoiceDate: '2026-02-03', + currency: 'USD', + totalAmount: '59.2100', + customerName: 'Customer A', + refCustomerId: null, + refCustomerUserId: null, + title: 'Sample Invoice', + description: 'Sample prepaid service', + memo: 'Thank you for choosing us!', + billTo: '101 Pine St, Ogdenville, USA, 44556', + shipTo: '654 Elm St, Shelbyville, USA, 09876', + shippingMethod: 'International Shipping (Tracking #: 901234)', + sellerInfo: + 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', + externalPaymentUrl: 'https://example.com/pay/123456790', + createdAt: '2026-02-03T11:33:49Z', + updatedAt: '2026-02-18T11:35:37Z', + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: 0, + timesContactedIn: 0, + timesContactedOut: 0, + paymentStatus: { + code: 'refunded', + label: 'Refunded', + isForced: false, + }, }, - }, - relationships: { - lineItems: { - data: [ - { - type: 'invoicelineitems', + relationships: { + lineItems: { + data: [ + { + type: 'invoicelineitems', + id: '1', + }, + { + type: 'invoicelineitems', + id: '2', + }, + { + type: 'invoicelineitems', + id: '3', + }, + ], + }, + owner: { + data: { + type: 'users', id: '1', }, - { - type: 'invoicelineitems', - id: '2', - }, - { - type: 'invoicelineitems', - id: '3', + }, + organization: { + data: { + type: 'organizations', + id: '1', }, - ], - }, - owner: { - data: { - type: 'users', - id: '1', }, - }, - organization: { - data: { - type: 'organizations', - id: '1', + customer: { + data: null, }, - }, - customer: { - data: null, - }, - customer_user: { - data: null, - }, - website: { - data: null, - }, - invoiceDefaultPdfFile: { - data: null, - }, - orders: { - data: [], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - internal_status: { - data: { - type: 'invoiceinternalstatuses', - id: 'draft', + customer_user: { + data: null, + }, + website: { + data: null, + }, + invoiceDefaultPdfFile: { + data: null, + }, + orders: { + data: [], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + internal_status: { + data: { + type: 'invoiceinternalstatuses', + id: 'draft', + }, }, }, + included: [] }, }; export const invoiceCreateData = { event: 'create', channel: 'invoice', - data: invoiceData, + eventData: invoiceData, timestamp: 1771523549, }; export const invoiceUpdateData = { event: 'update', channel: 'invoice', - data: invoiceData, + eventData: invoiceData, timestamp: 1771527786, }; export const invoiceDeleteData = { event: 'delete', channel: 'invoice', - data: invoiceData, + eventData: invoiceData, timestamp: 1771527301, }; diff --git a/packages/pieces/community/orocommerce/src/lib/examples/order.ts b/packages/pieces/community/orocommerce/src/lib/examples/order.ts index 799c916e5410..047bdfcee8a8 100644 --- a/packages/pieces/community/orocommerce/src/lib/examples/order.ts +++ b/packages/pieces/community/orocommerce/src/lib/examples/order.ts @@ -1,205 +1,208 @@ export const orderCreateData = { event: 'create', channel: 'order', - data: { - type: 'orders', - id: '48', - attributes: { - identifier: '48', - poNumber: 'PONUM111222334', - customerNotes: 'Customer Notes', - shipUntil: { - date: '2026-02-27 00:00:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - currency: 'USD', - subtotalWithDiscountsValue: '79.9900', - subtotalWithDiscounts: '79.9900', - subtotalValue: '79.9900', - totalValue: '89.9900', - shippingMethod: 'fixed_product_6', - shippingMethodType: 'primary', - estimatedShippingCostAmount: '10.0000', - overriddenShippingCostAmount: null, - sourceEntityIdentifier: null, - totalDiscountsAmount: null, - external: false, - createdAt: { - date: '2026-02-19 19:40:25.000000', - timezone_type: 3, - timezone: 'UTC', - }, - updatedAt: { - date: '2026-02-19 19:40:25.000000', - timezone_type: 3, - timezone: 'UTC', - }, - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: null, - timesContactedIn: null, - timesContactedOut: null, - disablePromotions: false, - discount: 0, - shippingDiscount: 0, - }, - relationships: { - createdBy: { - data: { - type: 'users', - id: '1', - }, - }, - billingAddress: { - data: { - type: 'orderaddresses', - id: '86', + eventData: { + data: { + type: 'orders', + id: '48', + attributes: { + identifier: '48', + poNumber: 'PONUM111222334', + customerNotes: 'Customer Notes', + shipUntil: { + date: '2026-02-27 00:00:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + currency: 'USD', + subtotalWithDiscountsValue: '79.9900', + subtotalWithDiscounts: '79.9900', + subtotalValue: '79.9900', + totalValue: '89.9900', + shippingMethod: 'fixed_product_6', + shippingMethodType: 'primary', + estimatedShippingCostAmount: '10.0000', + overriddenShippingCostAmount: null, + sourceEntityIdentifier: null, + totalDiscountsAmount: null, + external: false, + createdAt: { + date: '2026-02-19 19:40:25.000000', + timezone_type: 3, + timezone: 'UTC', + }, + updatedAt: { + date: '2026-02-19 19:40:25.000000', + timezone_type: 3, + timezone: 'UTC', + }, + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: null, + timesContactedIn: null, + timesContactedOut: null, + disablePromotions: false, + discount: 0, + shippingDiscount: 0, + }, + relationships: { + createdBy: { + data: { + type: 'users', + id: '1', + }, }, - }, - shippingAddress: { - data: { - type: 'orderaddresses', - id: '87', + billingAddress: { + data: { + type: 'orderaddresses', + id: '86', + }, }, - }, - website: { - data: { - type: 'websites', - id: '1', + shippingAddress: { + data: { + type: 'orderaddresses', + id: '87', + }, }, - }, - lineItems: { - data: [ - { - type: 'orderlineitems', - id: '129', + website: { + data: { + type: 'websites', + id: '1', }, - ], - }, - discounts: { - data: [], - }, - shippingTrackings: { - data: [], - }, - parent: { - data: null, - }, - subOrders: { - data: [], - }, - owner: { - data: { - type: 'users', - id: '1', }, - }, - organization: { - data: { - type: 'organizations', - id: '1', + lineItems: { + data: [ + { + type: 'orderlineitems', + id: '129', + }, + ], }, - }, - customerUser: { - data: { - type: 'customerusers', - id: '1', + discounts: { + data: [], }, - }, - customer: { - data: { - type: 'customers', - id: '5', + shippingTrackings: { + data: [], }, - }, - paymentTerm: { - data: { - type: 'paymentterms', - id: '1', + parent: { + data: null, }, - }, - invoices: { - data: [], - }, - warehouse: { - data: { - type: 'warehouses', - id: '1', - }, - }, - source: { - data: null, - }, - internalStatus: { - data: { - type: 'orderinternalstatuses', - id: 'order_internal_status.open', + subOrders: { + data: [], }, - }, - orderSubtotals: { - data: [ - { - type: 'ordersubtotals', - id: '48-subtotal-0', + owner: { + data: { + type: 'users', + id: '1', }, - { - type: 'ordersubtotals', - id: '48-discount-1', + }, + organization: { + data: { + type: 'organizations', + id: '1', }, - { - type: 'ordersubtotals', - id: '48-shipping_cost-2', + }, + customerUser: { + data: { + type: 'customerusers', + id: '1', }, - { - type: 'ordersubtotals', - id: '48-discount-3', + }, + customer: { + data: { + type: 'customers', + id: '5', }, - { - type: 'ordersubtotals', - id: '48-tax-4', + }, + paymentTerm: { + data: { + type: 'paymentterms', + id: '1', }, - { - type: 'ordersubtotals', - id: '48-tax-5', + }, + invoices: { + data: [], + }, + warehouse: { + data: { + type: 'warehouses', + id: '1', }, - { - type: 'ordersubtotals', - id: '48-tax-6', + }, + source: { + data: null, + }, + internalStatus: { + data: { + type: 'orderinternalstatuses', + id: 'order_internal_status.open', }, - ], - }, - attachments: { - data: [], - }, - documents: { - data: [ - { - type: 'files', - id: '661', - meta: { - sortOrder: 1, + }, + orderSubtotals: { + data: [ + { + type: 'ordersubtotals', + id: '48-subtotal-0', + }, + { + type: 'ordersubtotals', + id: '48-discount-1', }, + { + type: 'ordersubtotals', + id: '48-shipping_cost-2', + }, + { + type: 'ordersubtotals', + id: '48-discount-3', + }, + { + type: 'ordersubtotals', + id: '48-tax-4', + }, + { + type: 'ordersubtotals', + id: '48-tax-5', + }, + { + type: 'ordersubtotals', + id: '48-tax-6', + }, + ], + }, + attachments: { + data: [], + }, + documents: { + data: [ + { + type: 'files', + id: '661', + meta: { + sortOrder: 1, + }, + }, + ], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + status: { + data: null, + }, + shippingStatus: { + data: { + type: 'ordershippingstatuses', + id: 'order_shipping_status.not_shipped', }, - ], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - status: { - data: null, - }, - shippingStatus: { - data: { - type: 'ordershippingstatuses', - id: 'order_shipping_status.not_shipped', }, }, }, + included: [] }, timestamp: 1771530026, }; @@ -207,214 +210,217 @@ export const orderCreateData = { export const orderUpdateData = { event: 'update', channel: 'order', - data: { - type: 'orders', - id: '42', - attributes: { - identifier: '42', - poNumber: 'PONUM111222334', - customerNotes: 'Order Customer Notes', - shipUntil: { - date: '2026-02-28 00:00:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - currency: 'USD', - subtotalWithDiscountsValue: '236.9700', - subtotalWithDiscounts: '236.9700', - subtotalValue: '239.9700', - totalValue: '246.9700', - shippingMethod: 'fixed_product_6', - shippingMethodType: 'primary', - estimatedShippingCostAmount: '10.0000', - overriddenShippingCostAmount: null, - sourceEntityIdentifier: null, - totalDiscountsAmount: '3.0000', - external: false, - createdAt: { - date: '2026-02-19 17:52:28.000000', - timezone_type: 3, - timezone: 'UTC', - }, - updatedAt: { - date: '2026-02-19 19:03:06.000000', - timezone_type: 3, - timezone: 'UTC', - }, - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: null, - timesContactedIn: null, - timesContactedOut: null, - disablePromotions: false, - discount: 0, - shippingDiscount: 0, - }, - relationships: { - createdBy: { - data: { - type: 'users', - id: '1', - }, - }, - billingAddress: { - data: { - type: 'orderaddresses', - id: '74', - }, - }, - shippingAddress: { - data: { - type: 'orderaddresses', - id: '75', + eventData: { + data: { + type: 'orders', + id: '42', + attributes: { + identifier: '42', + poNumber: 'PONUM111222334', + customerNotes: 'Order Customer Notes', + shipUntil: { + date: '2026-02-28 00:00:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + currency: 'USD', + subtotalWithDiscountsValue: '236.9700', + subtotalWithDiscounts: '236.9700', + subtotalValue: '239.9700', + totalValue: '246.9700', + shippingMethod: 'fixed_product_6', + shippingMethodType: 'primary', + estimatedShippingCostAmount: '10.0000', + overriddenShippingCostAmount: null, + sourceEntityIdentifier: null, + totalDiscountsAmount: '3.0000', + external: false, + createdAt: { + date: '2026-02-19 17:52:28.000000', + timezone_type: 3, + timezone: 'UTC', + }, + updatedAt: { + date: '2026-02-19 19:03:06.000000', + timezone_type: 3, + timezone: 'UTC', + }, + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: null, + timesContactedIn: null, + timesContactedOut: null, + disablePromotions: false, + discount: 0, + shippingDiscount: 0, + }, + relationships: { + createdBy: { + data: { + type: 'users', + id: '1', + }, }, - }, - website: { - data: { - type: 'websites', - id: '1', + billingAddress: { + data: { + type: 'orderaddresses', + id: '74', + }, }, - }, - lineItems: { - data: [ - { - type: 'orderlineitems', - id: '123', + shippingAddress: { + data: { + type: 'orderaddresses', + id: '75', }, - ], - }, - discounts: { - data: [ - { - type: 'orderdiscounts', + }, + website: { + data: { + type: 'websites', id: '1', }, - ], - }, - shippingTrackings: { - data: [], - }, - parent: { - data: null, - }, - subOrders: { - data: [], - }, - owner: { - data: { - type: 'users', - id: '1', }, - }, - organization: { - data: { - type: 'organizations', - id: '1', - }, - }, - customerUser: { - data: { - type: 'customerusers', - id: '1', + lineItems: { + data: [ + { + type: 'orderlineitems', + id: '123', + }, + ], }, - }, - customer: { - data: { - type: 'customers', - id: '5', + discounts: { + data: [ + { + type: 'orderdiscounts', + id: '1', + }, + ], }, - }, - paymentTerm: { - data: { - type: 'paymentterms', - id: '1', + shippingTrackings: { + data: [], }, - }, - invoices: { - data: [], - }, - warehouse: { - data: { - type: 'warehouses', - id: '1', + parent: { + data: null, }, - }, - source: { - data: null, - }, - internalStatus: { - data: { - type: 'orderinternalstatuses', - id: 'order_internal_status.open', + subOrders: { + data: [], }, - }, - orderSubtotals: { - data: [ - { - type: 'ordersubtotals', - id: '42-subtotal-0', - }, - { - type: 'ordersubtotals', - id: '42-discount-1', + owner: { + data: { + type: 'users', + id: '1', }, - { - type: 'ordersubtotals', - id: '42-discount-2', + }, + organization: { + data: { + type: 'organizations', + id: '1', }, - { - type: 'ordersubtotals', - id: '42-shipping_cost-3', + }, + customerUser: { + data: { + type: 'customerusers', + id: '1', }, - { - type: 'ordersubtotals', - id: '42-discount-4', + }, + customer: { + data: { + type: 'customers', + id: '5', }, - { - type: 'ordersubtotals', - id: '42-tax-5', + }, + paymentTerm: { + data: { + type: 'paymentterms', + id: '1', }, - { - type: 'ordersubtotals', - id: '42-tax-6', + }, + invoices: { + data: [], + }, + warehouse: { + data: { + type: 'warehouses', + id: '1', }, - { - type: 'ordersubtotals', - id: '42-tax-7', + }, + source: { + data: null, + }, + internalStatus: { + data: { + type: 'orderinternalstatuses', + id: 'order_internal_status.open', }, - ], - }, - attachments: { - data: [], - }, - documents: { - data: [ - { - type: 'files', - id: '656', - meta: { - sortOrder: 1, + }, + orderSubtotals: { + data: [ + { + type: 'ordersubtotals', + id: '42-subtotal-0', + }, + { + type: 'ordersubtotals', + id: '42-discount-1', + }, + { + type: 'ordersubtotals', + id: '42-discount-2', + }, + { + type: 'ordersubtotals', + id: '42-shipping_cost-3', }, + { + type: 'ordersubtotals', + id: '42-discount-4', + }, + { + type: 'ordersubtotals', + id: '42-tax-5', + }, + { + type: 'ordersubtotals', + id: '42-tax-6', + }, + { + type: 'ordersubtotals', + id: '42-tax-7', + }, + ], + }, + attachments: { + data: [], + }, + documents: { + data: [ + { + type: 'files', + id: '656', + meta: { + sortOrder: 1, + }, + }, + ], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + status: { + data: null, + }, + shippingStatus: { + data: { + type: 'ordershippingstatuses', + id: 'order_shipping_status.not_shipped', }, - ], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - status: { - data: null, - }, - shippingStatus: { - data: { - type: 'ordershippingstatuses', - id: 'order_shipping_status.not_shipped', }, }, }, + included: [] }, timestamp: 1771527786, }; @@ -422,207 +428,210 @@ export const orderUpdateData = { export const orderDeleteData = { event: 'delete', channel: 'order', - data: { - type: 'orders', - id: '20', - attributes: { - identifier: 'FR1012408', - poNumber: 'RT104568EUR', - customerNotes: null, - shipUntil: { - date: '2026-02-18 00:00:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - currency: 'USD', - subtotalWithDiscountsValue: '33951.0000', - subtotalWithDiscounts: '33951.0000', - subtotalValue: '33951.0000', - totalValue: '33951.0000', - shippingMethod: null, - shippingMethodType: null, - estimatedShippingCostAmount: null, - overriddenShippingCostAmount: null, - sourceEntityIdentifier: null, - totalDiscountsAmount: null, - external: false, - createdAt: { - date: '2025-12-19 21:04:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - updatedAt: { - date: '2026-02-18 11:35:37.000000', - timezone_type: 3, - timezone: 'UTC', - }, - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: 0, - timesContactedIn: 0, - timesContactedOut: 0, - disablePromotions: false, - discount: 0, - shippingDiscount: 0, - }, - relationships: { - createdBy: { - data: { - type: 'users', - id: '4', - }, - }, - billingAddress: { - data: { - type: 'orderaddresses', - id: '27', - }, - }, - shippingAddress: { - data: { - type: 'orderaddresses', - id: '28', - }, - }, - website: { - data: { - type: 'websites', - id: '1', + eventData: { + data: { + type: 'orders', + id: '20', + attributes: { + identifier: 'FR1012408', + poNumber: 'RT104568EUR', + customerNotes: null, + shipUntil: { + date: '2026-02-18 00:00:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + currency: 'USD', + subtotalWithDiscountsValue: '33951.0000', + subtotalWithDiscounts: '33951.0000', + subtotalValue: '33951.0000', + totalValue: '33951.0000', + shippingMethod: null, + shippingMethodType: null, + estimatedShippingCostAmount: null, + overriddenShippingCostAmount: null, + sourceEntityIdentifier: null, + totalDiscountsAmount: null, + external: false, + createdAt: { + date: '2025-12-19 21:04:00.000000', + timezone_type: 3, + timezone: 'UTC', + }, + updatedAt: { + date: '2026-02-18 11:35:37.000000', + timezone_type: 3, + timezone: 'UTC', + }, + lastContactedDate: null, + lastContactedDateIn: null, + lastContactedDateOut: null, + timesContacted: 0, + timesContactedIn: 0, + timesContactedOut: 0, + disablePromotions: false, + discount: 0, + shippingDiscount: 0, + }, + relationships: { + createdBy: { + data: { + type: 'users', + id: '4', + }, }, - }, - lineItems: { - data: [ - { - type: 'orderlineitems', - id: '65', + billingAddress: { + data: { + type: 'orderaddresses', + id: '27', }, - { - type: 'orderlineitems', - id: '66', + }, + shippingAddress: { + data: { + type: 'orderaddresses', + id: '28', }, - { - type: 'orderlineitems', - id: '67', + }, + website: { + data: { + type: 'websites', + id: '1', }, - ], - }, - discounts: { - data: [], - }, - shippingTrackings: { - data: [], - }, - parent: { - data: null, - }, - subOrders: { - data: [], - }, - owner: { - data: { - type: 'users', - id: '21', }, - }, - organization: { - data: { - type: 'organizations', - id: '1', + lineItems: { + data: [ + { + type: 'orderlineitems', + id: '65', + }, + { + type: 'orderlineitems', + id: '66', + }, + { + type: 'orderlineitems', + id: '67', + }, + ], }, - }, - customerUser: { - data: { - type: 'customerusers', - id: '11', + discounts: { + data: [], }, - }, - customer: { - data: { - type: 'customers', - id: '11', + shippingTrackings: { + data: [], }, - }, - paymentTerm: { - data: { - type: 'paymentterms', - id: '3', + parent: { + data: null, }, - }, - invoices: { - data: [ - { - type: 'invoices', - id: '20', - }, - ], - }, - warehouse: { - data: null, - }, - source: { - data: null, - }, - internalStatus: { - data: { - type: 'orderinternalstatuses', - id: 'order_internal_status.open', + subOrders: { + data: [], }, - }, - orderSubtotals: { - data: [ - { - type: 'ordersubtotals', - id: '20-subtotal-0', + owner: { + data: { + type: 'users', + id: '21', }, - { - type: 'ordersubtotals', - id: '20-discount-1', + }, + organization: { + data: { + type: 'organizations', + id: '1', }, - { - type: 'ordersubtotals', - id: '20-shipping_cost-2', + }, + customerUser: { + data: { + type: 'customerusers', + id: '11', }, - { - type: 'ordersubtotals', - id: '20-discount-3', + }, + customer: { + data: { + type: 'customers', + id: '11', }, - { - type: 'ordersubtotals', - id: '20-tax-4', + }, + paymentTerm: { + data: { + type: 'paymentterms', + id: '3', }, - { - type: 'ordersubtotals', - id: '20-tax-5', + }, + invoices: { + data: [ + { + type: 'invoices', + id: '20', + }, + ], + }, + warehouse: { + data: null, + }, + source: { + data: null, + }, + internalStatus: { + data: { + type: 'orderinternalstatuses', + id: 'order_internal_status.open', }, - { - type: 'ordersubtotals', - id: '20-tax-6', + }, + orderSubtotals: { + data: [ + { + type: 'ordersubtotals', + id: '20-subtotal-0', + }, + { + type: 'ordersubtotals', + id: '20-discount-1', + }, + { + type: 'ordersubtotals', + id: '20-shipping_cost-2', + }, + { + type: 'ordersubtotals', + id: '20-discount-3', + }, + { + type: 'ordersubtotals', + id: '20-tax-4', + }, + { + type: 'ordersubtotals', + id: '20-tax-5', + }, + { + type: 'ordersubtotals', + id: '20-tax-6', + }, + ], + }, + attachments: { + data: [], + }, + documents: { + data: [], + }, + activityNotes: { + data: [], + }, + activityEmails: { + data: [], + }, + status: { + data: null, + }, + shippingStatus: { + data: { + type: 'ordershippingstatuses', + id: 'order_shipping_status.not_shipped', }, - ], - }, - attachments: { - data: [], - }, - documents: { - data: [], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - status: { - data: null, - }, - shippingStatus: { - data: { - type: 'ordershippingstatuses', - id: 'order_shipping_status.not_shipped', }, }, }, + included: [] }, timestamp: 1771527301, }; From 6745075798fbec3a14814bcc6f90da4e71207d67 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Tue, 3 Mar 2026 11:49:55 +0100 Subject: [PATCH 09/50] feat(orocommerce): removed not needed properties for order & invoice --- .../src/lib/actions/create-invoice.ts | 51 +------- .../src/lib/actions/create-order.ts | 123 +++--------------- 2 files changed, 22 insertions(+), 152 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index e6ae137f37d8..c71da6b2ec7e 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -35,18 +35,7 @@ export const createInvoiceAction = createAction({ required: true, }), customer: customerDropdown, - customerId: Property.ShortText({ - displayName: 'Customer: Raw ID', - description: - 'Overrides the Customer dropdown. Paste an ID from a previous step.', - required: false, - }), customerUser: customerUserDropdown(false), - customerUserId: Property.ShortText({ - displayName: 'Customer User: Raw ID', - description: 'Overrides the Customer User dropdown.', - required: false, - }), refCustomerId: Property.Number({ displayName: 'External Customer ID', description: @@ -121,24 +110,8 @@ export const createInvoiceAction = createAction({ required: false, }), owner: userDropdown, - ownerId: Property.ShortText({ - displayName: 'Owner: Raw ID', - description: 'Overrides the Owner dropdown.', - required: false, - }), website: websiteDropdown, - websiteId: Property.ShortText({ - displayName: 'Website: Raw ID', - description: 'Overrides the Website dropdown.', - required: false, - }), internalStatus: invoiceInternalStatusDropdown, - internalStatusId: Property.ShortText({ - displayName: 'Internal Status: Raw ID', - description: - 'Overrides the Internal Status dropdown (e.g. draft, open, paid).', - required: false, - }), // -- Line Items ------------------------------------------------------------ // DynamicProperties lets us load product units once as StaticDropdown. @@ -202,18 +175,6 @@ export const createInvoiceAction = createAction({ async run(context) { const p = context.propsValue; - // raw ID takes priority over dropdown selection when both are present - const resolve = ( - rawId: string | null | undefined, - dropdownVal: unknown - ): string | null => { - const raw = rawId?.trim(); - if (raw) return raw; - if (dropdownVal != null && String(dropdownVal).trim()) - return String(dropdownVal).trim(); - return null; - }; - // -- Build included line items with string IDs -------------------------- // DynamicProperties wraps the array in an object keyed by "lineItems" const dynamicValue = (p.lineItems ?? {}) as Record; @@ -268,38 +229,38 @@ export const createInvoiceAction = createAction({ lineItems: { data: lineItemsRelData }, }; - const customerId = resolve(p.customerId, p.customer); + const customerId = p.customer; if (customerId) { relationships['customer'] = { data: { type: 'customers', id: customerId }, }; } - const customerUserId = resolve(p.customerUserId, p.customerUser); + const customerUserId = p.customerUser; if (customerUserId) { relationships['customer_user'] = { data: { type: 'customerusers', id: customerUserId }, }; } - const organizationId = resolve(p.organizationId, p.organization); + const organizationId = p.organization; if (organizationId) { relationships['organization'] = { data: { type: 'organizations', id: organizationId }, }; } - const ownerId = resolve(p.ownerId, p.owner); + const ownerId = p.owner; if (ownerId) { relationships['owner'] = { data: { type: 'users', id: ownerId } }; } - const websiteId = resolve(p.websiteId, p.website); + const websiteId = p.website; if (websiteId) { relationships['website'] = { data: { type: 'websites', id: websiteId } }; } - const internalStatusId = resolve(p.internalStatusId, p.internalStatus); + const internalStatusId = p.internalStatus; if (internalStatusId) { relationships['internal_status'] = { data: { type: 'invoiceinternalstatuses', id: internalStatusId }, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index d89cd77647b1..a6eb4c634542 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -27,11 +27,6 @@ export const createOrderAction = createAction({ props: { // -- Required relationships ------------------------------------------------ customer: customerRequiredDropdown, - customerId: Property.ShortText({ - displayName: 'Customer: Raw ID', - description: 'Overrides the Customer dropdown. Paste an ID from a previous step.', - required: false, - }), // -- Optional attributes --------------------------------------------------- currency: Property.ShortText({ @@ -88,59 +83,14 @@ export const createOrderAction = createAction({ // -- Optional relationships ------------------------------------------------ customerUser: customerUserDropdown(false), - customerUserId: Property.ShortText({ - displayName: 'Customer User: Raw ID', - description: 'Overrides the Customer User dropdown.', - required: false, - }), organization: organizationDropdown, - organizationId: Property.ShortText({ - displayName: 'Organization: Raw ID', - description: 'Overrides the Organization dropdown.', - required: false, - }), owner: userDropdown, - ownerId: Property.ShortText({ - displayName: 'Owner: Raw ID', - description: 'Overrides the Owner dropdown.', - required: false, - }), website: websiteDropdown, - websiteId: Property.ShortText({ - displayName: 'Website: Raw ID', - description: 'Overrides the Website dropdown.', - required: false, - }), internalStatus: orderInternalStatusDropdown, - internalStatusId: Property.ShortText({ - displayName: 'Internal Status: Raw ID', - description: 'Overrides the Internal Status dropdown (e.g. order_internal_status.open).', - required: false, - }), paymentTerm: paymentTermDropdown, - paymentTermId: Property.ShortText({ - displayName: 'Payment Term: Raw ID', - description: 'Overrides the Payment Term dropdown.', - required: false, - }), warehouse: warehouseDropdown, - warehouseId: Property.ShortText({ - displayName: 'Warehouse: Raw ID', - description: 'Overrides the Warehouse dropdown.', - required: false, - }), parent: orderDropdown, - parentId: Property.ShortText({ - displayName: 'Parent Order: Raw ID', - description: 'Overrides the Parent Order dropdown. Paste an order ID from a previous step.', - required: false, - }), status: orderStatusDropdown, - statusId: Property.ShortText({ - displayName: 'Status: Raw ID', - description: 'Overrides the Status dropdown. Only applied when "Enable External Status Management" is on.', - required: false, - }), // -- Billing Address ------------------------------------------------------- billingAddressLabel: Property.ShortText({ @@ -181,21 +131,11 @@ export const createOrderAction = createAction({ required: false, }), billingAddressCountry: buildCountryDropdown(false, 'Billing: Country'), - billingAddressCountryId: Property.ShortText({ - displayName: 'Billing: Country Raw ID', - description: 'Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).', - required: false, - }), billingAddressRegion: buildRegionDropdown( 'billingAddressCountry', false, 'Billing: Region / State' ), - billingAddressRegionId: Property.ShortText({ - displayName: 'Billing: Region Raw ID', - description: 'Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).', - required: false, - }), billingAddressCustomRegion: Property.ShortText({ displayName: 'Billing: Custom Region', description: 'Free-text region for countries without predefined regions.', @@ -241,21 +181,11 @@ export const createOrderAction = createAction({ required: false, }), shippingAddressCountry: buildCountryDropdown(false, 'Shipping: Country'), - shippingAddressCountryId: Property.ShortText({ - displayName: 'Shipping: Country Raw ID', - description: 'Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).', - required: false, - }), shippingAddressRegion: buildRegionDropdown( 'shippingAddressCountry', false, 'Shipping: Region / State' ), - shippingAddressRegionId: Property.ShortText({ - displayName: 'Shipping: Region Raw ID', - description: 'Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).', - required: false, - }), shippingAddressCustomRegion: Property.ShortText({ displayName: 'Shipping: Custom Region', description: 'Free-text region for countries without predefined regions.', @@ -345,12 +275,6 @@ export const createOrderAction = createAction({ required: true, options: { disabled: false, options: unitOptions }, }), - productUnitRawId: Property.ShortText({ - displayName: 'Product Unit: Raw ID', - description: - 'Overrides the Product Unit dropdown (e.g. "piece", "kg").', - required: false, - }), quantity: Property.Number({ displayName: 'Quantity', description: 'Quantity of the product ordered.', @@ -407,11 +331,6 @@ export const createOrderAction = createAction({ required: false, options: { disabled: false, options: warehouseOptions }, }), - warehouseId: Property.ShortText({ - displayName: 'Warehouse: Raw ID', - description: 'Overrides the Warehouse dropdown.', - required: false, - }), }, }), }; @@ -426,14 +345,6 @@ export const createOrderAction = createAction({ async run(context) { const p = context.propsValue; - // raw ID takes priority over dropdown selection when both are present - const resolve = (rawId: string | null | undefined, dropdownVal: unknown): string | null => { - const raw = rawId?.trim(); - if (raw) return raw; - if (dropdownVal != null && String(dropdownVal).trim()) return String(dropdownVal).trim(); - return null; - }; - const included: Record[] = []; const relationships: Record = {}; @@ -495,8 +406,8 @@ export const createOrderAction = createAction({ street2: p.billingAddressStreet2, city: p.billingAddressCity, postalCode: p.billingAddressPostalCode, - country: resolve(p.billingAddressCountryId, p.billingAddressCountry), - region: resolve(p.billingAddressRegionId, p.billingAddressRegion), + country: p.billingAddressCountry, + region: p.billingAddressRegion, customRegion: p.billingAddressCustomRegion, }); if (billingAdded) { @@ -516,8 +427,8 @@ export const createOrderAction = createAction({ street2: p.shippingAddressStreet2, city: p.shippingAddressCity, postalCode: p.shippingAddressPostalCode, - country: resolve(p.shippingAddressCountryId, p.shippingAddressCountry), - region: resolve(p.shippingAddressRegionId, p.shippingAddressRegion), + country: p.shippingAddressCountry, + region: p.shippingAddressRegion, customRegion: p.shippingAddressCustomRegion, }); if (shippingAdded) { @@ -534,9 +445,7 @@ export const createOrderAction = createAction({ const lineItemsRelData = rawItems.map((item, index) => { const lid = `li_${index + 1}`; - const productUnitId = (item['productUnitRawId'] as string | undefined)?.trim() - || (item['productUnit'] as string | undefined) - || 'piece'; + const productUnitId = item['productUnit'] as string | undefined; const liAttributes: Record = { productSku: String(item['productSku'] ?? ''), @@ -593,53 +502,53 @@ export const createOrderAction = createAction({ if (p.shippingMethodType) attributes['shippingMethodType'] = p.shippingMethodType; if (p.disablePromotions) attributes['disablePromotions'] = p.disablePromotions; - // -- Order relationships — each resolved via raw ID → dropdown fallback -- - const customerId = resolve(p.customerId, p.customer); + // -- Order relationships -- + const customerId = p.customer; if (customerId) { relationships['customer'] = { data: { type: 'customers', id: customerId } }; } - const customerUserId = resolve(p.customerUserId, p.customerUser); + const customerUserId = p.customerUser; if (customerUserId) { relationships['customerUser'] = { data: { type: 'customerusers', id: customerUserId } }; } - const organizationId = resolve(p.organizationId, p.organization); + const organizationId = p.organization; if (organizationId) { relationships['organization'] = { data: { type: 'organizations', id: organizationId } }; } - const ownerId = resolve(p.ownerId, p.owner); + const ownerId = p.owner; if (ownerId) { relationships['owner'] = { data: { type: 'users', id: ownerId } }; } - const websiteId = resolve(p.websiteId, p.website); + const websiteId = p.website; if (websiteId) { relationships['website'] = { data: { type: 'websites', id: websiteId } }; } - const internalStatusId = resolve(p.internalStatusId, p.internalStatus); + const internalStatusId = p.internalStatus; if (internalStatusId) { relationships['internalStatus'] = { data: { type: 'orderinternalstatuses', id: internalStatusId } }; } - const paymentTermId = resolve(p.paymentTermId, p.paymentTerm); + const paymentTermId = p.paymentTerm; if (paymentTermId) { relationships['paymentTerm'] = { data: { type: 'paymentterms', id: paymentTermId } }; } - const warehouseId = resolve(p.warehouseId, p.warehouse); + const warehouseId = p.warehouse; if (warehouseId) { relationships['warehouse'] = { data: { type: 'warehouses', id: warehouseId } }; } - const parentId = resolve(p.parentId, p.parent); + const parentId = p.parent; if (parentId) { relationships['parent'] = { data: { type: 'orders', id: parentId } }; } - const statusId = resolve(p.statusId, p.status); + const statusId = p.status; if (statusId) { relationships['status'] = { data: { type: 'orderstatuses', id: statusId } }; } From 1d78d30f77ec9c2ddd8fc9a67851e0ac9ded32ce Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Tue, 10 Mar 2026 16:27:05 +0100 Subject: [PATCH 10/50] feat(orocommerce): updated example responses --- .../orocommerce/src/lib/examples/invoice.ts | 14 +++++++------- .../orocommerce/src/lib/examples/order.ts | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts index 53707e6844b8..afbb92251bc1 100644 --- a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts @@ -91,27 +91,27 @@ const invoiceData = { }, }, }, - included: [] }, + included: [] }; export const invoiceCreateData = { - event: 'create', - channel: 'invoice', + topic: 'invoice.create', eventData: invoiceData, timestamp: 1771523549, + messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' }; export const invoiceUpdateData = { - event: 'update', - channel: 'invoice', + topic: 'invoice.update', eventData: invoiceData, timestamp: 1771527786, + messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' }; export const invoiceDeleteData = { - event: 'delete', - channel: 'invoice', + topic: 'invoice.delete', eventData: invoiceData, timestamp: 1771527301, + messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' }; diff --git a/packages/pieces/community/orocommerce/src/lib/examples/order.ts b/packages/pieces/community/orocommerce/src/lib/examples/order.ts index 047bdfcee8a8..3ee8f74ab115 100644 --- a/packages/pieces/community/orocommerce/src/lib/examples/order.ts +++ b/packages/pieces/community/orocommerce/src/lib/examples/order.ts @@ -1,6 +1,5 @@ export const orderCreateData = { - event: 'create', - channel: 'order', + topic: 'order.create', eventData: { data: { type: 'orders', @@ -205,11 +204,11 @@ export const orderCreateData = { included: [] }, timestamp: 1771530026, + messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' }; export const orderUpdateData = { - event: 'update', - channel: 'order', + topic: 'order.update', eventData: { data: { type: 'orders', @@ -420,14 +419,14 @@ export const orderUpdateData = { }, }, }, - included: [] + included: [], }, timestamp: 1771527786, + messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' }; export const orderDeleteData = { - event: 'delete', - channel: 'order', + topic: 'order.delete', eventData: { data: { type: 'orders', @@ -631,7 +630,8 @@ export const orderDeleteData = { }, }, }, - included: [] + included: [], }, timestamp: 1771527301, + messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' }; From 82f7293c2eefce42826d285dee66a406c449696c Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Tue, 10 Mar 2026 18:41:49 +0100 Subject: [PATCH 11/50] feat(orocommerce): activepieces 0.79.* piece architecture support --- packages/pieces/community/orocommerce/package.json | 14 +++++++++++++- .../pieces/community/orocommerce/tsconfig.json | 1 - .../pieces/community/orocommerce/tsconfig.lib.json | 8 +++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index 0f67e61ad8b0..45b2d5353ba3 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -1,4 +1,16 @@ { "name": "@activepieces/piece-orocommerce", - "version": "0.0.2" + "version": "0.2.0", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + }, + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "2.6.2" + } } diff --git a/packages/pieces/community/orocommerce/tsconfig.json b/packages/pieces/community/orocommerce/tsconfig.json index eff240ac143e..29c9dd1bfc1c 100644 --- a/packages/pieces/community/orocommerce/tsconfig.json +++ b/packages/pieces/community/orocommerce/tsconfig.json @@ -4,7 +4,6 @@ "module": "commonjs", "forceConsistentCasingInFileNames": true, "strict": true, - "importHelpers": true, "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, diff --git a/packages/pieces/community/orocommerce/tsconfig.lib.json b/packages/pieces/community/orocommerce/tsconfig.lib.json index 5995d959bf0c..0ba4caeb858b 100644 --- a/packages/pieces/community/orocommerce/tsconfig.lib.json +++ b/packages/pieces/community/orocommerce/tsconfig.lib.json @@ -1,9 +1,15 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", "declaration": true, + "declarationMap": true, "types": ["node"] }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], "include": ["src/**/*.ts"] } From 11f1005197c43e118c3e21d0ee169697db5cc113 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 11 Mar 2026 20:04:05 +0100 Subject: [PATCH 12/50] feat(orocommerce): update to follow latest orocommerce APIs --- .../pieces/community/orocommerce/src/index.ts | 2 ++ .../orocommerce/src/lib/common/client.ts | 9 +++++++-- .../src/lib/common/register-webhook.ts | 15 +++++---------- .../orocommerce/src/lib/triggers/invoice.ts | 17 +++++++++++------ .../orocommerce/src/lib/triggers/order.ts | 9 +++------ 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index ac49df717ce8..d1c644b9928a 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -6,6 +6,7 @@ import { newInvoice, removedInvoice, updatedInvoice, + paymentForInvoice, } from './lib/triggers/invoice'; import { createInvoiceAction, @@ -38,5 +39,6 @@ export const orocommerce = createPiece({ newInvoice, removedInvoice, updatedInvoice, + paymentForInvoice, ], }); diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index a4effb16a819..b5f93268c335 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -22,9 +22,10 @@ async function getAccessToken(auth: OroAuth): Promise { return cachedToken; } + const baseUrl = auth.props.serverUrl.replace(/\/*$/, ''); const response = await httpClient.sendRequest({ method: HttpMethod.POST, - url: auth.props.serverUrl + '/oauth2-token', + url: `${baseUrl}/oauth2-token`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -50,9 +51,13 @@ export async function oroApiCall({ headers: extraHeaders, }: OroApiCallParams): Promise> { try { + const serverUrl = auth.props.serverUrl.replace(/\/*$/, ''); + const adminPrefix = auth.props.adminPrefix.replace(/^\/+|\/+$/g, ''); + const resource = resourceUri.replace(/^\/+/, ''); + return await httpClient.sendRequest({ method, - url: `${auth.props.serverUrl}/${auth.props.adminPrefix}/api${resourceUri}`, + url: `${serverUrl}/${adminPrefix}/api/${resource}`, headers: { 'Content-Type': 'application/vnd.api+json', ...extraHeaders, diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts index 97cd08297a58..3718804e67a3 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts @@ -12,14 +12,12 @@ export const createOroWebhookTrigger = ({ description, displayName, topic, - event, sampleData, }: { name: string; description: string; displayName: string; topic: string; - event: string; sampleData: Record; }): Trigger => createTrigger({ @@ -34,14 +32,13 @@ export const createOroWebhookTrigger = ({ async onEnable(context) { const response = await oroApiCall({ method: HttpMethod.POST, - resourceUri: '/remotewebhooks', + resourceUri: '/webhookproducersettings', auth: context.auth, body: { data: { - type: 'remotewebhooks', + type: 'webhookproducersettings', attributes: { - channel: topic, - event, + topic: topic, enabled: true, notificationUrl: context.webhookUrl, }, @@ -53,8 +50,7 @@ export const createOroWebhookTrigger = ({ `_orocommerce_${name}_trigger`, { webhookId: response.body.data.id, - topic, - event, + topic } ); }, @@ -67,7 +63,7 @@ export const createOroWebhookTrigger = ({ if (webhookInfo !== null && webhookInfo !== undefined) { await oroApiCall({ method: HttpMethod.DELETE, - resourceUri: `/remotewebhooks/${webhookInfo.webhookId}`, + resourceUri: `/webhookproducersettings/${webhookInfo.webhookId}`, auth: context.auth, }); } @@ -81,5 +77,4 @@ export const createOroWebhookTrigger = ({ interface WebhookInformation { webhookId: string; topic: string; - event: string; } diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts b/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts index 28ad13ba6ad1..40033dcec500 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts @@ -4,8 +4,7 @@ import { invoiceCreateData, invoiceUpdateData, invoiceDeleteData } from '../exam export const newInvoice = createOroWebhookTrigger({ name: 'new_invoice', description: 'Triggered when a new invoice is created', - topic: 'invoice', - event: 'create', + topic: 'invoice.create', displayName: 'New Invoice', sampleData: invoiceCreateData, }); @@ -13,8 +12,7 @@ export const newInvoice = createOroWebhookTrigger({ export const updatedInvoice = createOroWebhookTrigger({ name: 'updated_invoice', description: 'Triggered when an invoice is updated', - topic: 'invoice', - event: 'update', + topic: 'invoice.update', displayName: 'Invoice Update', sampleData: invoiceUpdateData, }); @@ -22,8 +20,15 @@ export const updatedInvoice = createOroWebhookTrigger({ export const removedInvoice = createOroWebhookTrigger({ name: 'removed_invoice', description: 'Triggered when an invoice is deleted', - topic: 'invoice', - event: 'delete', + topic: 'invoice.delete', displayName: 'Invoice Removal', sampleData: invoiceDeleteData, }); + +export const paymentForInvoice = createOroWebhookTrigger({ + name: 'invoice_payment', + description: 'Triggered when an invoice payment status is changed', + topic: 'invoice.payment_status_change', + displayName: 'Invoice Payment Status Changed', + sampleData: invoiceDeleteData, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/order.ts b/packages/pieces/community/orocommerce/src/lib/triggers/order.ts index de67d171f13a..5e47c18d85d7 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/order.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/order.ts @@ -4,8 +4,7 @@ import { orderCreateData, orderUpdateData, orderDeleteData } from '../examples/o export const newOrder = createOroWebhookTrigger({ name: 'new_order', description: 'Triggered when a new order is created', - topic: 'order', - event: 'create', + topic: 'order.create', displayName: 'New Order', sampleData: orderCreateData, }); @@ -13,8 +12,7 @@ export const newOrder = createOroWebhookTrigger({ export const updatedOrder = createOroWebhookTrigger({ name: 'updated_order', description: 'Triggered when an order is updated', - topic: 'order', - event: 'update', + topic: 'order.update', displayName: 'Order Update', sampleData: orderUpdateData, }); @@ -22,8 +20,7 @@ export const updatedOrder = createOroWebhookTrigger({ export const removedOrder = createOroWebhookTrigger({ name: 'removed_order', description: 'Triggered when an order is deleted', - topic: 'order', - event: 'delete', + topic: 'order.delete', displayName: 'Order Removal', sampleData: orderDeleteData, }); From 850c5903b7e031d11db439cd1474c25c073606b6 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Tue, 17 Mar 2026 14:53:23 +0100 Subject: [PATCH 13/50] feat(orocommerce): update to follow latest orocommerce APIs --- .../orocommerce/src/lib/common/register-webhook.ts | 7 ++++--- .../community/orocommerce/src/lib/triggers/invoice.ts | 8 ++++---- .../community/orocommerce/src/lib/triggers/order.ts | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts index 3718804e67a3..a18f690f1d8c 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts @@ -32,14 +32,15 @@ export const createOroWebhookTrigger = ({ async onEnable(context) { const response = await oroApiCall({ method: HttpMethod.POST, - resourceUri: '/webhookproducersettings', + resourceUri: '/webhooks', auth: context.auth, body: { data: { - type: 'webhookproducersettings', + type: 'webhooks', attributes: { topic: topic, enabled: true, + format: 'default', notificationUrl: context.webhookUrl, }, }, @@ -63,7 +64,7 @@ export const createOroWebhookTrigger = ({ if (webhookInfo !== null && webhookInfo !== undefined) { await oroApiCall({ method: HttpMethod.DELETE, - resourceUri: `/webhookproducersettings/${webhookInfo.webhookId}`, + resourceUri: `/webhooks/${webhookInfo.webhookId}`, auth: context.auth, }); } diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts b/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts index 40033dcec500..80836c2fee5a 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts @@ -4,7 +4,7 @@ import { invoiceCreateData, invoiceUpdateData, invoiceDeleteData } from '../exam export const newInvoice = createOroWebhookTrigger({ name: 'new_invoice', description: 'Triggered when a new invoice is created', - topic: 'invoice.create', + topic: 'invoice.created', displayName: 'New Invoice', sampleData: invoiceCreateData, }); @@ -12,7 +12,7 @@ export const newInvoice = createOroWebhookTrigger({ export const updatedInvoice = createOroWebhookTrigger({ name: 'updated_invoice', description: 'Triggered when an invoice is updated', - topic: 'invoice.update', + topic: 'invoice.updated', displayName: 'Invoice Update', sampleData: invoiceUpdateData, }); @@ -20,7 +20,7 @@ export const updatedInvoice = createOroWebhookTrigger({ export const removedInvoice = createOroWebhookTrigger({ name: 'removed_invoice', description: 'Triggered when an invoice is deleted', - topic: 'invoice.delete', + topic: 'invoice.deleted', displayName: 'Invoice Removal', sampleData: invoiceDeleteData, }); @@ -28,7 +28,7 @@ export const removedInvoice = createOroWebhookTrigger({ export const paymentForInvoice = createOroWebhookTrigger({ name: 'invoice_payment', description: 'Triggered when an invoice payment status is changed', - topic: 'invoice.payment_status_change', + topic: 'invoice.payment_status_changed', displayName: 'Invoice Payment Status Changed', sampleData: invoiceDeleteData, }); diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/order.ts b/packages/pieces/community/orocommerce/src/lib/triggers/order.ts index 5e47c18d85d7..048596aa81ab 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/order.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/order.ts @@ -4,7 +4,7 @@ import { orderCreateData, orderUpdateData, orderDeleteData } from '../examples/o export const newOrder = createOroWebhookTrigger({ name: 'new_order', description: 'Triggered when a new order is created', - topic: 'order.create', + topic: 'order.created', displayName: 'New Order', sampleData: orderCreateData, }); @@ -12,7 +12,7 @@ export const newOrder = createOroWebhookTrigger({ export const updatedOrder = createOroWebhookTrigger({ name: 'updated_order', description: 'Triggered when an order is updated', - topic: 'order.update', + topic: 'order.updated', displayName: 'Order Update', sampleData: orderUpdateData, }); @@ -20,7 +20,7 @@ export const updatedOrder = createOroWebhookTrigger({ export const removedOrder = createOroWebhookTrigger({ name: 'removed_order', description: 'Triggered when an order is deleted', - topic: 'order.delete', + topic: 'order.deleted', displayName: 'Order Removal', sampleData: orderDeleteData, }); From 39c6dda823cc3e3369d914eb7a898ce75c7b6af2 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 19 Mar 2026 10:58:21 +0100 Subject: [PATCH 14/50] feat(orocommerce): update to follow latest orocommerce APIs --- .../src/lib/common/register-webhook.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts index a18f690f1d8c..700f3e7cd2d7 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts @@ -38,11 +38,23 @@ export const createOroWebhookTrigger = ({ data: { type: 'webhooks', attributes: { - topic: topic, enabled: true, - format: 'default', notificationUrl: context.webhookUrl, }, + relationships: { + topic: { + data: { + type: 'webhooktopics', + id: topic, + }, + }, + format: { + data: { + type: 'webhookformats', + id: 'default', + }, + }, + }, }, }, }); From 69bf9cbf3b0668f1e4cf358bea306d385a727e1e Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 19 Mar 2026 14:53:07 +0100 Subject: [PATCH 15/50] feat(orocommerce): update to follow latest orocommerce APIs --- .../pieces/community/orocommerce/src/lib/actions/api-call.ts | 2 +- packages/pieces/community/orocommerce/src/lib/common/auth.ts | 2 +- .../community/orocommerce/src/lib/common/register-webhook.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts index 518ef7143fbe..8cb735609d96 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -24,7 +24,7 @@ export const apiCallAction = createAction({ resourceUri: Property.ShortText({ displayName: 'Resource URI', description: - 'Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open', + 'Path relative to the API base, e.g. orders, orders/42, invoices?filter[status]=open', required: true, }), headers: Property.Json({ diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 9d992d8e2f6a..9fee7f64c09b 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -48,7 +48,7 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. try { await oroApiCall({ method: HttpMethod.GET, - resourceUri: '/regions/US-CA', + resourceUri: 'regions/US-CA', auth: { type: AppConnectionType.CUSTOM_AUTH, props: auth, diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts index 700f3e7cd2d7..2609de4ebe7b 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts @@ -32,7 +32,7 @@ export const createOroWebhookTrigger = ({ async onEnable(context) { const response = await oroApiCall({ method: HttpMethod.POST, - resourceUri: '/webhooks', + resourceUri: 'webhooks', auth: context.auth, body: { data: { @@ -76,7 +76,7 @@ export const createOroWebhookTrigger = ({ if (webhookInfo !== null && webhookInfo !== undefined) { await oroApiCall({ method: HttpMethod.DELETE, - resourceUri: `/webhooks/${webhookInfo.webhookId}`, + resourceUri: `webhooks/${webhookInfo.webhookId}`, auth: context.auth, }); } From ef41363b3f3c363c1a13baab6cbcd2aae75835c2 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Fri, 20 Mar 2026 14:17:23 +0100 Subject: [PATCH 16/50] feat(orocommerce): update to follow latest orocommerce APIs --- .../pieces/community/orocommerce/package.json | 8 ++++---- .../community/orocommerce/src/i18n/de.json | 14 -------------- .../community/orocommerce/src/i18n/fr.json | 14 -------------- .../community/orocommerce/src/i18n/nl.json | 14 -------------- .../community/orocommerce/src/i18n/pl.json | 14 -------------- .../orocommerce/src/i18n/translation.json | 16 +--------------- .../community/orocommerce/src/i18n/uk.json | 14 -------------- .../src/lib/actions/create-invoice.ts | 5 ----- 8 files changed, 5 insertions(+), 94 deletions(-) diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index 45b2d5353ba3..ace5d480c788 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -3,14 +3,14 @@ "version": "0.2.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", - "scripts": { - "build": "tsc -p tsconfig.lib.json && cp package.json dist/", - "lint": "eslint 'src/**/*.ts'" - }, "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", "@activepieces/shared": "workspace:*", "tslib": "2.6.2" + }, + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" } } diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json index ddff5e6cd879..c1c5c9044958 100644 --- a/packages/pieces/community/orocommerce/src/i18n/de.json +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -23,9 +23,7 @@ "Currency": "Währung", "Customer Name": "Kundenname", "Customer": "Kunde", - "Customer: Raw ID": "Kunde: Roh-ID", "Customer User": "Kundenbenutzer", - "Customer User: Raw ID": "Kundenbenutzer: Roh-ID", "External Customer ID": "Externe Kunden-ID", "External Customer User ID": "Externe Kundenbenutzer-ID", "Total Amount": "Gesamtbetrag", @@ -39,13 +37,9 @@ "Seller Info": "Verkäuferinformationen", "External Payment URL": "Externe Zahlungs-URL", "Organization": "Organisation", - "Organization: Raw ID": "Organisation: Roh-ID", "User": "Benutzer", - "Owner: Raw ID": "Inhaber: Roh-ID", "Website": "Website", - "Website: Raw ID": "Website: Roh-ID", "Internal Status": "Interner Status", - "Internal Status: Raw ID": "Interner Status: Roh-ID", "Line Items": "Positionen", "Identifier": "Bezeichner", "PO Number": "Bestellnummer (PO)", @@ -56,13 +50,9 @@ "Shipping Method Type": "Typ der Versandmethode", "Disable Promotions": "Aktionen deaktivieren", "Payment Term": "Zahlungsbedingung", - "Payment Term: Raw ID": "Zahlungsbedingung: Roh-ID", "Warehouse": "Lager", - "Warehouse: Raw ID": "Lager: Roh-ID", "Parent Order": "Übergeordnete Bestellung", - "Parent Order: Raw ID": "Übergeordnete Bestellung: Roh-ID", "Status": "Status", - "Status: Raw ID": "Status: Roh-ID", "Billing: Label": "Rechnung: Bezeichnung", "Billing: First Name": "Rechnung: Vorname", "Billing: Last Name": "Rechnung: Nachname", @@ -73,9 +63,7 @@ "Billing: City": "Rechnung: Stadt", "Billing: Postal Code": "Rechnung: Postleitzahl", "Billing: Country": "Rechnung: Land", - "Billing: Country Raw ID": "Rechnung: Land Roh-ID", "Billing: Region / State": "Rechnung: Region / Bundesland", - "Billing: Region Raw ID": "Rechnung: Region Roh-ID", "Billing: Custom Region": "Rechnung: Benutzerdefinierte Region", "Shipping: Label": "Versand: Bezeichnung", "Shipping: First Name": "Versand: Vorname", @@ -87,9 +75,7 @@ "Shipping: City": "Versand: Stadt", "Shipping: Postal Code": "Versand: Postleitzahl", "Shipping: Country": "Versand: Land", - "Shipping: Country Raw ID": "Versand: Land Roh-ID", "Shipping: Region / State": "Versand: Region / Bundesland", - "Shipping: Region Raw ID": "Versand: Region Roh-ID", "Shipping: Custom Region": "Versand: Benutzerdefinierte Region", "Product": "Produkt", "Method": "Methode", diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json index a69b0e4a51eb..b56c9ce9e0c0 100644 --- a/packages/pieces/community/orocommerce/src/i18n/fr.json +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -23,9 +23,7 @@ "Currency": "Devise", "Customer Name": "Nom du client", "Customer": "Client", - "Customer: Raw ID": "Client : ID brut", "Customer User": "Utilisateur client", - "Customer User: Raw ID": "Utilisateur client : ID brut", "External Customer ID": "ID client externe", "External Customer User ID": "ID d'utilisateur client externe", "Total Amount": "Montant total", @@ -39,13 +37,9 @@ "Seller Info": "Informations vendeur", "External Payment URL": "URL de paiement externe", "Organization": "Organisation", - "Organization: Raw ID": "Organisation : ID brut", "User": "Utilisateur", - "Owner: Raw ID": "Propriétaire : ID brut", "Website": "Site web", - "Website: Raw ID": "Site web : ID brut", "Internal Status": "Statut interne", - "Internal Status: Raw ID": "Statut interne : ID brut", "Line Items": "Lignes", "Identifier": "Identifiant", "PO Number": "Numéro de commande (PO)", @@ -56,13 +50,9 @@ "Shipping Method Type": "Type de méthode d'expédition", "Disable Promotions": "Désactiver les promotions", "Payment Term": "Condition de paiement", - "Payment Term: Raw ID": "Condition de paiement : ID brut", "Warehouse": "Entrepôt", - "Warehouse: Raw ID": "Entrepôt : ID brut", "Parent Order": "Commande parente", - "Parent Order: Raw ID": "Commande parente : ID brut", "Status": "Statut", - "Status: Raw ID": "Statut : ID brut", "Billing: Label": "Facturation : libellé", "Billing: First Name": "Facturation : prénom", "Billing: Last Name": "Facturation : nom", @@ -73,9 +63,7 @@ "Billing: City": "Facturation : ville", "Billing: Postal Code": "Facturation : code postal", "Billing: Country": "Facturation : pays", - "Billing: Country Raw ID": "Facturation : pays ID brut", "Billing: Region / State": "Facturation : région / État", - "Billing: Region Raw ID": "Facturation : région ID brut", "Billing: Custom Region": "Facturation : région personnalisée", "Shipping: Label": "Expédition : libellé", "Shipping: First Name": "Expédition : prénom", @@ -87,9 +75,7 @@ "Shipping: City": "Expédition : ville", "Shipping: Postal Code": "Expédition : code postal", "Shipping: Country": "Expédition : pays", - "Shipping: Country Raw ID": "Expédition : pays ID brut", "Shipping: Region / State": "Expédition : région / État", - "Shipping: Region Raw ID": "Expédition : région ID brut", "Shipping: Custom Region": "Expédition : région personnalisée", "Product": "Produit", "Method": "Méthode", diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json index cf9412c8e3e1..86e06d6f89d1 100644 --- a/packages/pieces/community/orocommerce/src/i18n/nl.json +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -23,9 +23,7 @@ "Currency": "Valuta", "Customer Name": "Naam klant", "Customer": "Klant", - "Customer: Raw ID": "Klant: ruwe ID", "Customer User": "Klantgebruiker", - "Customer User: Raw ID": "Klantgebruiker: ruwe ID", "External Customer ID": "Externe klant-ID", "External Customer User ID": "Externe klantgebruiker-ID", "Total Amount": "Totaalbedrag", @@ -39,13 +37,9 @@ "Seller Info": "Verkoperinfo", "External Payment URL": "Externe betalings-URL", "Organization": "Organisatie", - "Organization: Raw ID": "Organisatie: ruwe ID", "User": "Gebruiker", - "Owner: Raw ID": "Eigenaar: ruwe ID", "Website": "Website", - "Website: Raw ID": "Website: ruwe ID", "Internal Status": "Interne status", - "Internal Status: Raw ID": "Interne status: ruwe ID", "Line Items": "Regelitems", "Identifier": "Identificatie", "PO Number": "Inkoopordernummer", @@ -56,13 +50,9 @@ "Shipping Method Type": "Type verzendmethode", "Disable Promotions": "Promoties uitschakelen", "Payment Term": "Betalingstermijn", - "Payment Term: Raw ID": "Betalingstermijn: ruwe ID", "Warehouse": "Magazijn", - "Warehouse: Raw ID": "Magazijn: ruwe ID", "Parent Order": "Bovenliggende order", - "Parent Order: Raw ID": "Bovenliggende order: ruwe ID", "Status": "Status", - "Status: Raw ID": "Status: ruwe ID", "Billing: Label": "Facturatie: label", "Billing: First Name": "Facturatie: voornaam", "Billing: Last Name": "Facturatie: achternaam", @@ -73,9 +63,7 @@ "Billing: City": "Facturatie: stad", "Billing: Postal Code": "Facturatie: postcode", "Billing: Country": "Facturatie: land", - "Billing: Country Raw ID": "Facturatie: land ruwe ID", "Billing: Region / State": "Facturatie: regio / staat", - "Billing: Region Raw ID": "Facturatie: regio ruwe ID", "Billing: Custom Region": "Facturatie: aangepaste regio", "Shipping: Label": "Verzending: label", "Shipping: First Name": "Verzending: voornaam", @@ -87,9 +75,7 @@ "Shipping: City": "Verzending: stad", "Shipping: Postal Code": "Verzending: postcode", "Shipping: Country": "Verzending: land", - "Shipping: Country Raw ID": "Verzending: land ruwe ID", "Shipping: Region / State": "Verzending: regio / staat", - "Shipping: Region Raw ID": "Verzending: regio ruwe ID", "Shipping: Custom Region": "Verzending: aangepaste regio", "Product": "Product", "Method": "Methode", diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json index 85e4f85b05ad..6cabe22d8638 100644 --- a/packages/pieces/community/orocommerce/src/i18n/pl.json +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -23,9 +23,7 @@ "Currency": "Waluta", "Customer Name": "Nazwa klienta", "Customer": "Klient", - "Customer: Raw ID": "Klient: surowe ID", "Customer User": "Użytkownik klienta", - "Customer User: Raw ID": "Użytkownik klienta: surowe ID", "External Customer ID": "Zewnętrzne ID klienta", "External Customer User ID": "Zewnętrzne ID użytkownika klienta", "Total Amount": "Kwota łączna", @@ -39,13 +37,9 @@ "Seller Info": "Informacje o sprzedawcy", "External Payment URL": "Zewnętrzny URL płatności", "Organization": "Organizacja", - "Organization: Raw ID": "Organizacja: surowe ID", "User": "Użytkownik", - "Owner: Raw ID": "Właściciel: surowe ID", "Website": "Strona internetowa", - "Website: Raw ID": "Strona internetowa: surowe ID", "Internal Status": "Status wewnętrzny", - "Internal Status: Raw ID": "Status wewnętrzny: surowe ID", "Line Items": "Pozycje", "Identifier": "Identyfikator", "PO Number": "Numer zamówienia zakupu (PO)", @@ -56,13 +50,9 @@ "Shipping Method Type": "Typ metody wysyłki", "Disable Promotions": "Wyłącz promocje", "Payment Term": "Termin płatności", - "Payment Term: Raw ID": "Termin płatności: surowe ID", "Warehouse": "Magazyn", - "Warehouse: Raw ID": "Magazyn: surowe ID", "Parent Order": "Zamówienie nadrzędne", - "Parent Order: Raw ID": "Zamówienie nadrzędne: surowe ID", "Status": "Status", - "Status: Raw ID": "Status: surowe ID", "Billing: Label": "Fakturowanie: etykieta", "Billing: First Name": "Fakturowanie: imię", "Billing: Last Name": "Fakturowanie: nazwisko", @@ -73,9 +63,7 @@ "Billing: City": "Fakturowanie: miasto", "Billing: Postal Code": "Fakturowanie: kod pocztowy", "Billing: Country": "Fakturowanie: kraj", - "Billing: Country Raw ID": "Fakturowanie: kraj (surowe ID)", "Billing: Region / State": "Fakturowanie: region / województwo", - "Billing: Region Raw ID": "Fakturowanie: region (surowe ID)", "Billing: Custom Region": "Fakturowanie: własny region", "Shipping: Label": "Wysyłka: etykieta", "Shipping: First Name": "Wysyłka: imię", @@ -87,9 +75,7 @@ "Shipping: City": "Wysyłka: miasto", "Shipping: Postal Code": "Wysyłka: kod pocztowy", "Shipping: Country": "Wysyłka: kraj", - "Shipping: Country Raw ID": "Wysyłka: kraj (surowe ID)", "Shipping: Region / State": "Wysyłka: region / województwo", - "Shipping: Region Raw ID": "Wysyłka: region (surowe ID)", "Shipping: Custom Region": "Wysyłka: własny region", "Product": "Produkt", "Method": "Metoda", diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json index d1dc1cad6504..ef43911e5bbe 100644 --- a/packages/pieces/community/orocommerce/src/i18n/translation.json +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -23,9 +23,7 @@ "Currency": "Currency", "Customer Name": "Customer Name", "Customer": "Customer", - "Customer: Raw ID": "Customer: Raw ID", "Customer User": "Customer User", - "Customer User: Raw ID": "Customer User: Raw ID", "External Customer ID": "External Customer ID", "External Customer User ID": "External Customer User ID", "Total Amount": "Total Amount", @@ -39,13 +37,9 @@ "Seller Info": "Seller Info", "External Payment URL": "External Payment URL", "Organization": "Organization", - "Organization: Raw ID": "Organization: Raw ID", "User": "User", - "Owner: Raw ID": "Owner: Raw ID", "Website": "Website", - "Website: Raw ID": "Website: Raw ID", "Internal Status": "Internal Status", - "Internal Status: Raw ID": "Internal Status: Raw ID", "Line Items": "Line Items", "Identifier": "Identifier", "PO Number": "PO Number", @@ -56,13 +50,9 @@ "Shipping Method Type": "Shipping Method Type", "Disable Promotions": "Disable Promotions", "Payment Term": "Payment Term", - "Payment Term: Raw ID": "Payment Term: Raw ID", "Warehouse": "Warehouse", - "Warehouse: Raw ID": "Warehouse: Raw ID", "Parent Order": "Parent Order", - "Parent Order: Raw ID": "Parent Order: Raw ID", "Status": "Status", - "Status: Raw ID": "Status: Raw ID", "Billing: Label": "Billing: Label", "Billing: First Name": "Billing: First Name", "Billing: Last Name": "Billing: Last Name", @@ -73,9 +63,7 @@ "Billing: City": "Billing: City", "Billing: Postal Code": "Billing: Postal Code", "Billing: Country": "Billing: Country", - "Billing: Country Raw ID": "Billing: Country Raw ID", "Billing: Region / State": "Billing: Region / State", - "Billing: Region Raw ID": "Billing: Region Raw ID", "Billing: Custom Region": "Billing: Custom Region", "Shipping: Label": "Shipping: Label", "Shipping: First Name": "Shipping: First Name", @@ -87,9 +75,7 @@ "Shipping: City": "Shipping: City", "Shipping: Postal Code": "Shipping: Postal Code", "Shipping: Country": "Shipping: Country", - "Shipping: Country Raw ID": "Shipping: Country Raw ID", "Shipping: Region / State": "Shipping: Region / State", - "Shipping: Region Raw ID": "Shipping: Region Raw ID", "Shipping: Custom Region": "Shipping: Custom Region", "Product": "Product", "Method": "Method", @@ -188,4 +174,4 @@ "Triggered when a new invoice is created": "Triggered when a new invoice is created", "Triggered when an invoice is deleted": "Triggered when an invoice is deleted", "Triggered when an invoice is updated": "Triggered when an invoice is updated" -} \ No newline at end of file +} diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json index 5ddb2d9f5103..0aeace870c2f 100644 --- a/packages/pieces/community/orocommerce/src/i18n/uk.json +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -23,9 +23,7 @@ "Currency": "Валюта", "Customer Name": "Назва клієнта", "Customer": "Клієнт", - "Customer: Raw ID": "Клієнт: сирий ID", "Customer User": "Користувач клієнта", - "Customer User: Raw ID": "Користувач клієнта: сирий ID", "External Customer ID": "Зовнішній ID клієнта", "External Customer User ID": "Зовнішній ID користувача клієнта", "Total Amount": "Загальна сума", @@ -39,13 +37,9 @@ "Seller Info": "Інформація про продавця", "External Payment URL": "Зовнішній URL оплати", "Organization": "Організація", - "Organization: Raw ID": "Організація: сирий ID", "User": "Користувач", - "Owner: Raw ID": "Власник: сирий ID", "Website": "Вебсайт", - "Website: Raw ID": "Вебсайт: сирий ID", "Internal Status": "Внутрішній статус", - "Internal Status: Raw ID": "Внутрішній статус: сирий ID", "Line Items": "Позиції", "Identifier": "Ідентифікатор", "PO Number": "Номер PO", @@ -56,13 +50,9 @@ "Shipping Method Type": "Тип способу доставки", "Disable Promotions": "Вимкнути промоакції", "Payment Term": "Умови оплати", - "Payment Term: Raw ID": "Умови оплати: сирий ID", "Warehouse": "Склад", - "Warehouse: Raw ID": "Склад: сирий ID", "Parent Order": "Батьківське замовлення", - "Parent Order: Raw ID": "Батьківське замовлення: сирий ID", "Status": "Статус", - "Status: Raw ID": "Статус: сирий ID", "Billing: Label": "Платіжна адреса: мітка", "Billing: First Name": "Платіжна адреса: ім’я", "Billing: Last Name": "Платіжна адреса: прізвище", @@ -73,9 +63,7 @@ "Billing: City": "Платіжна адреса: місто", "Billing: Postal Code": "Платіжна адреса: поштовий індекс", "Billing: Country": "Платіжна адреса: країна", - "Billing: Country Raw ID": "Платіжна адреса: країна (сирий ID)", "Billing: Region / State": "Платіжна адреса: регіон / штат", - "Billing: Region Raw ID": "Платіжна адреса: регіон (сирий ID)", "Billing: Custom Region": "Платіжна адреса: інший регіон", "Shipping: Label": "Адреса доставки: мітка", "Shipping: First Name": "Адреса доставки: ім’я", @@ -87,9 +75,7 @@ "Shipping: City": "Адреса доставки: місто", "Shipping: Postal Code": "Адреса доставки: поштовий індекс", "Shipping: Country": "Адреса доставки: країна", - "Shipping: Country Raw ID": "Адреса доставки: країна (сирий ID)", "Shipping: Region / State": "Адреса доставки: регіон / штат", - "Shipping: Region Raw ID": "Адреса доставки: регіон (сирий ID)", "Shipping: Custom Region": "Адреса доставки: інший регіон", "Product": "Товар", "Method": "Метод", diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index c71da6b2ec7e..533c6b9bf9b5 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -104,11 +104,6 @@ export const createInvoiceAction = createAction({ }), organization: organizationDropdown, - organizationId: Property.ShortText({ - displayName: 'Organization: Raw ID', - description: 'Overrides the Organization dropdown.', - required: false, - }), owner: userDropdown, website: websiteDropdown, internalStatus: invoiceInternalStatusDropdown, From 4da52506fed94c53a888d474602c17e78bd84ab4 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 9 Apr 2026 18:19:41 +0200 Subject: [PATCH 17/50] feat(pieces): oro commerce triggers replaced with a single webhook trigger --- .../pieces/community/orocommerce/src/index.ts | 18 +-- .../orocommerce/src/lib/common/index.ts | 1 - .../src/lib/common/register-webhook.ts | 93 --------------- .../orocommerce/src/lib/triggers/invoice.ts | 34 ------ .../orocommerce/src/lib/triggers/order.ts | 26 ----- .../src/lib/triggers/webhook-topic-trigger.ts | 106 ++++++++++++++++++ 6 files changed, 108 insertions(+), 170 deletions(-) delete mode 100644 packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts delete mode 100644 packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts delete mode 100644 packages/pieces/community/orocommerce/src/lib/triggers/order.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index d1c644b9928a..30b4f48773bf 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -1,13 +1,7 @@ import { createPiece } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/shared'; import { oroAuth } from './lib/common'; -import { newOrder, removedOrder, updatedOrder } from './lib/triggers/order'; -import { - newInvoice, - removedInvoice, - updatedInvoice, - paymentForInvoice, -} from './lib/triggers/invoice'; +import { oroWebhookTopicTrigger } from './lib/triggers/webhook-topic-trigger'; import { createInvoiceAction, createOrderAction, @@ -32,13 +26,5 @@ export const orocommerce = createPiece({ serializeJsonApiAction, unserializeJsonApiAction, ], - triggers: [ - newOrder, - removedOrder, - updatedOrder, - newInvoice, - removedInvoice, - updatedInvoice, - paymentForInvoice, - ], + triggers: [oroWebhookTopicTrigger], }); diff --git a/packages/pieces/community/orocommerce/src/lib/common/index.ts b/packages/pieces/community/orocommerce/src/lib/common/index.ts index 8e86756d7ebf..8a442b37b9ad 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -1,4 +1,3 @@ export * from './auth'; export * from './client'; -export * from './register-webhook'; export * from './props'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts b/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts deleted file mode 100644 index 2609de4ebe7b..000000000000 --- a/packages/pieces/community/orocommerce/src/lib/common/register-webhook.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - createTrigger, - Trigger, - TriggerStrategy, -} from '@activepieces/pieces-framework'; -import { oroAuth } from './auth'; -import { oroApiCall } from './client'; -import { HttpMethod } from '@activepieces/pieces-common'; - -export const createOroWebhookTrigger = ({ - name, - description, - displayName, - topic, - sampleData, -}: { - name: string; - description: string; - displayName: string; - topic: string; - sampleData: Record; -}): Trigger => - createTrigger({ - auth: oroAuth, - name, - description, - displayName, - props: {}, - sampleData, - type: TriggerStrategy.WEBHOOK, - - async onEnable(context) { - const response = await oroApiCall({ - method: HttpMethod.POST, - resourceUri: 'webhooks', - auth: context.auth, - body: { - data: { - type: 'webhooks', - attributes: { - enabled: true, - notificationUrl: context.webhookUrl, - }, - relationships: { - topic: { - data: { - type: 'webhooktopics', - id: topic, - }, - }, - format: { - data: { - type: 'webhookformats', - id: 'default', - }, - }, - }, - }, - }, - }); - - await context.store.put( - `_orocommerce_${name}_trigger`, - { - webhookId: response.body.data.id, - topic - } - ); - }, - - async onDisable(context) { - const webhookInfo = await context.store.get( - `_orocommerce_${name}_trigger` - ); - - if (webhookInfo !== null && webhookInfo !== undefined) { - await oroApiCall({ - method: HttpMethod.DELETE, - resourceUri: `webhooks/${webhookInfo.webhookId}`, - auth: context.auth, - }); - } - }, - - async run(context) { - return [context.payload.body]; - }, - }); - -interface WebhookInformation { - webhookId: string; - topic: string; -} diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts b/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts deleted file mode 100644 index 80836c2fee5a..000000000000 --- a/packages/pieces/community/orocommerce/src/lib/triggers/invoice.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createOroWebhookTrigger } from '../common/register-webhook'; -import { invoiceCreateData, invoiceUpdateData, invoiceDeleteData } from '../examples/invoice'; - -export const newInvoice = createOroWebhookTrigger({ - name: 'new_invoice', - description: 'Triggered when a new invoice is created', - topic: 'invoice.created', - displayName: 'New Invoice', - sampleData: invoiceCreateData, -}); - -export const updatedInvoice = createOroWebhookTrigger({ - name: 'updated_invoice', - description: 'Triggered when an invoice is updated', - topic: 'invoice.updated', - displayName: 'Invoice Update', - sampleData: invoiceUpdateData, -}); - -export const removedInvoice = createOroWebhookTrigger({ - name: 'removed_invoice', - description: 'Triggered when an invoice is deleted', - topic: 'invoice.deleted', - displayName: 'Invoice Removal', - sampleData: invoiceDeleteData, -}); - -export const paymentForInvoice = createOroWebhookTrigger({ - name: 'invoice_payment', - description: 'Triggered when an invoice payment status is changed', - topic: 'invoice.payment_status_changed', - displayName: 'Invoice Payment Status Changed', - sampleData: invoiceDeleteData, -}); diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/order.ts b/packages/pieces/community/orocommerce/src/lib/triggers/order.ts deleted file mode 100644 index 048596aa81ab..000000000000 --- a/packages/pieces/community/orocommerce/src/lib/triggers/order.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createOroWebhookTrigger } from '../common/register-webhook'; -import { orderCreateData, orderUpdateData, orderDeleteData } from '../examples/order'; - -export const newOrder = createOroWebhookTrigger({ - name: 'new_order', - description: 'Triggered when a new order is created', - topic: 'order.created', - displayName: 'New Order', - sampleData: orderCreateData, -}); - -export const updatedOrder = createOroWebhookTrigger({ - name: 'updated_order', - description: 'Triggered when an order is updated', - topic: 'order.updated', - displayName: 'Order Update', - sampleData: orderUpdateData, -}); - -export const removedOrder = createOroWebhookTrigger({ - name: 'removed_order', - description: 'Triggered when an order is deleted', - topic: 'order.deleted', - displayName: 'Order Removal', - sampleData: orderDeleteData, -}); diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts new file mode 100644 index 000000000000..c7543c1e4b59 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -0,0 +1,106 @@ +import { + createTrigger, + Property, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { oroAuth, oroApiCall } from '../common'; +import { OroAuth, OroJsonApiCollection, OroJsonApiItem } from '../common/types'; +import { HttpMethod } from '@activepieces/pieces-common'; + +export const oroWebhookTopicTrigger = createTrigger({ + auth: oroAuth, + name: 'oro-webhook-event', + displayName: 'Oro Webhook Event', + description: 'Trigger when a selected webhook event is raised', + props: { + topic: Property.Dropdown({ + auth: oroAuth, + displayName: 'Topic', + description: 'Only topics accessible by your connection are shown', + required: true, + refreshers: ['auth'], // re-fetches when connection changes + options: async ({ auth }) => { + if (!auth) { + return { disabled: true, placeholder: 'Connect your account first', options: [] }; + } + + const response = await oroApiCall({ + method: HttpMethod.GET, + resourceUri: 'webhooktopics', + auth: auth as OroAuth + }); + const body = response.body as OroJsonApiCollection; + + return { + options: (body.data ?? []).map((item: OroJsonApiItem) => ({ + label: String( + item.attributes['label'] ? item.attributes['label'] + ' (' + item.id + ')' : item.id + ), + value: item.id, + })), + }; + }, + }), + }, + type: TriggerStrategy.WEBHOOK, + sampleData: {}, + + async onEnable(context) { + const response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: 'webhooks', + auth: context.auth, + body: { + data: { + type: 'webhooks', + attributes: { + enabled: true, + notificationUrl: context.webhookUrl, + }, + relationships: { + topic: { + data: { + type: 'webhooktopics', + id: context.propsValue.topic, + }, + }, + format: { + data: { + type: 'webhookformats', + id: 'default', + }, + }, + }, + }, + }, + }); + + await context.store.put(`webhookInfo`, { + webhookId: response.body.data.id, + topic: context.propsValue.topic, + }); + }, + + async onDisable(context) { + const webhookInfo = await context.store.get( + `webhookInfo` + ); + + if (webhookInfo !== null && webhookInfo !== undefined) { + await oroApiCall({ + method: HttpMethod.DELETE, + resourceUri: `webhooks/${webhookInfo.webhookId}`, + auth: context.auth, + }); + } + }, + + async run(context) { + return [context.payload.body]; + }, +}); + +interface WebhookInformation { + webhookId: string; + topic: string; +} From ffbec76d413699057e557cd69d91bf4797823b28 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 30 Apr 2026 17:30:11 +0200 Subject: [PATCH 18/50] feat(orocommerce): updated api --- .../orocommerce/src/lib/common/auth.ts | 20 ++--- .../orocommerce/src/lib/common/client.ts | 75 ++++++++++------ .../orocommerce/src/lib/common/props.ts | 90 +++++++++---------- .../orocommerce/src/lib/common/types.ts | 6 ++ 4 files changed, 109 insertions(+), 82 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 9fee7f64c09b..1b705f422f1e 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -1,7 +1,7 @@ import { PieceAuth, Property } from '@activepieces/pieces-framework'; -import { HttpMethod } from '@activepieces/pieces-common'; +import { HttpMethod, HttpResponse, HttpMessageBody } from '@activepieces/pieces-common'; import { oroApiCall } from './client'; -import { AppConnectionType } from '@activepieces/shared'; +import { AppConnectionType, tryCatch } from '@activepieces/shared'; export const oroAuth = PieceAuth.CustomAuth({ description: ` @@ -44,25 +44,25 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. }), }, - validate: async ({ auth }) => { - try { - await oroApiCall({ + validate: async ({ auth }): Promise<{ valid: true } | { valid: false; error: string }> => { + const { error } = await tryCatch>(() => + oroApiCall({ method: HttpMethod.GET, resourceUri: 'regions/US-CA', auth: { type: AppConnectionType.CUSTOM_AUTH, props: auth, }, - }); - return { valid: true }; - } catch (e: any) { + }), + ); + if (error) { return { valid: false, - error: - e?.message || + error: error.message || 'Invalid credentials. Please verify your Server URL, Admin Prefix, Client ID, and Client Secret.', }; } + return { valid: true }; }, required: true, diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index b5f93268c335..5192f3c885b6 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -3,6 +3,7 @@ import { HttpMethod, HttpMessageBody, HttpResponse, + HttpError, AuthenticationType, } from '@activepieces/pieces-common'; @@ -10,16 +11,38 @@ import { type OroAuth, type OroAuthResponseType, type OroApiCallParams, - OroJsonApiItem, - OroJsonApiCollection, + type OroJsonApiItem, + type OroJsonApiCollection, + type FetchCollectionParams, } from './types'; -let cachedToken: string | null = null; -let tokenExpiresAt = 0; +const tokenCache = new Map(); -async function getAccessToken(auth: OroAuth): Promise { - if (cachedToken && Date.now() < tokenExpiresAt) { - return cachedToken; +function buildCacheKey({ auth }: { auth: OroAuth }): string { + return `${auth.props.serverUrl}::${auth.props.clientId}`; +} + +function formatError({ error }: { error: unknown }): string { + if (error instanceof HttpError) { + const status = error.response.status; + const body = error.response.body; + const detail = typeof body === 'object' && body !== null + ? JSON.stringify(body) + : String(body ?? ''); + return `OroCommerce API Error (${status}): ${detail}`; + } + if (error instanceof Error) { + return `OroCommerce API Error: ${error.message}`; + } + return `OroCommerce API Error: ${String(error)}`; +} + +async function getAccessToken({ auth }: { auth: OroAuth }): Promise { + const cacheKey = buildCacheKey({ auth }); + const cached = tokenCache.get(cacheKey); + + if (cached && Date.now() < cached.expiresAt) { + return cached.token; } const baseUrl = auth.props.serverUrl.replace(/\/*$/, ''); @@ -29,17 +52,20 @@ async function getAccessToken(auth: OroAuth): Promise { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - body: { + body: new URLSearchParams({ grant_type: 'client_credentials', client_id: auth.props.clientId, client_secret: auth.props.clientSecret, - }, + }).toString(), }); - cachedToken = response.body.access_token; - tokenExpiresAt = Date.now() + response.body.expires_in * 1000 - 30 * 1000; + const token = response.body.access_token; + tokenCache.set(cacheKey, { + token, + expiresAt: Date.now() + response.body.expires_in * 1000 - 30_000, + }); - return cachedToken; + return token; } export async function oroApiCall({ @@ -64,27 +90,21 @@ export async function oroApiCall({ }, authentication: { type: AuthenticationType.BEARER_TOKEN, - token: await getAccessToken(auth), + token: await getAccessToken({ auth }), }, queryParams, body, }); - } catch (error: any) { - const statusCode = error.response?.status; - const errorMessage = - error.response?.data?.message || error.message || 'Unknown error'; - - throw new Error( - `OroCommerce API Error (${statusCode || 'Unknown'}): ${errorMessage}` - ); + } catch (error: unknown) { + throw new Error(formatError({ error })); } } -export async function fetchCollection( - auth: OroAuth, - resourceUri: string, - queryParams?: Record -): Promise { +export async function fetchCollection({ + auth, + resourceUri, + queryParams, +}: FetchCollectionParams): Promise { const response = await oroApiCall({ method: HttpMethod.GET, resourceUri, @@ -92,5 +112,6 @@ export async function fetchCollection( queryParams: { 'page[size]': '50', ...queryParams }, }); - return (response.body as OroJsonApiCollection).data ?? []; + const body = response.body as OroJsonApiCollection | undefined; + return body?.data ?? []; } diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index a2f6a85572d6..12d1da893c36 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -43,11 +43,11 @@ function buildCustomerOptions( params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/customers', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/customers', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -101,11 +101,11 @@ export const customerUserDropdown = (required = false) => } params['filter[searchQuery]'] = searchFilters.join(' and '); - const items = await fetchCollection( - auth as OroAuth, - '/customerusers', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/customerusers', + queryParams: params, + }); return { options: items.map((item) => { const firstName = String(item.attributes['firstName'] ?? ''); @@ -142,11 +142,11 @@ export const organizationDropdown = Property.Dropdown({ params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/organizations', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/organizations', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -177,7 +177,7 @@ export const userDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection(auth as OroAuth, '/users', params); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/users', queryParams: params }); return { options: items.map((item) => { const firstName = String(item.attributes['firstName'] ?? ''); @@ -214,7 +214,7 @@ export const websiteDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection(auth as OroAuth, '/websites', params); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/websites', queryParams: params }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -239,10 +239,10 @@ export const invoiceInternalStatusDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection( - auth as OroAuth, - '/invoiceinternalstatuses' - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/invoiceinternalstatuses', + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -267,10 +267,10 @@ export const orderInternalStatusDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection( - auth as OroAuth, - '/orderinternalstatuses' - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/orderinternalstatuses', + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -368,11 +368,11 @@ export const paymentTermDropdown = Property.Dropdown({ params['filter[searchQuery]'] = `label ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/paymentterms', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/paymentterms', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['label'] ?? item.id), @@ -404,11 +404,11 @@ export const warehouseDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/warehouses', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/warehouses', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -441,11 +441,11 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') 'page[size]': '300', }; - const items = await fetchCollection( - auth as OroAuth, - '/countries', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/countries', + queryParams: params, + }); return { options: items .filter(function (item) { @@ -501,7 +501,7 @@ export const buildRegionDropdown = ( try { const params: Record = {'filter[country]': countryId}; - const items = await fetchCollection(auth, '/regions', params); + const items = await fetchCollection({ auth, resourceUri: '/regions', queryParams: params }); return { options: items .filter(function (item) { @@ -537,7 +537,7 @@ export const orderStatusDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection(auth as OroAuth, '/orderstatuses'); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/orderstatuses' }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -568,7 +568,7 @@ export const orderDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection(auth as OroAuth, '/orders', params); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/orders', queryParams: params }); return { options: items.map((item) => ({ label: String( @@ -597,9 +597,9 @@ export const productUnitDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection(auth as OroAuth, '/productunits', { + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/productunits', queryParams: { 'page[size]': '100', - }); + } }); return { options: items.map((item) => ({ // productunits use the string code as id (e.g. "each"), attributes may have label diff --git a/packages/pieces/community/orocommerce/src/lib/common/types.ts b/packages/pieces/community/orocommerce/src/lib/common/types.ts index 36d6fba7e052..d2a96c03eb1a 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/types.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -25,6 +25,12 @@ export type OroApiCallParams = { headers?: Record; }; +export type FetchCollectionParams = { + auth: OroAuth; + resourceUri: string; + queryParams?: Record; +}; + export interface OroJsonApiItem { id: string; type: string; From c9db164c187283377f8f748d293f4c6b2d20cefd Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 6 May 2026 17:52:57 +0200 Subject: [PATCH 19/50] feat(orocommerce): updated custom api call --- .../pieces/community/orocommerce/src/index.ts | 4 +- .../orocommerce/src/lib/actions/api-call.ts | 97 +++---------------- .../orocommerce/src/lib/actions/index.ts | 2 +- .../orocommerce/src/lib/common/client.ts | 12 ++- 4 files changed, 23 insertions(+), 92 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index 30b4f48773bf..fa95c9ffb828 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -5,7 +5,7 @@ import { oroWebhookTopicTrigger } from './lib/triggers/webhook-topic-trigger'; import { createInvoiceAction, createOrderAction, - apiCallAction, + customApiCallAction, serializeJsonApiAction, unserializeJsonApiAction, } from './lib/actions'; @@ -22,7 +22,7 @@ export const orocommerce = createPiece({ actions: [ createInvoiceAction, createOrderAction, - apiCallAction, + customApiCallAction, serializeJsonApiAction, unserializeJsonApiAction, ], diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts index 8cb735609d96..7dd5b29dc673 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -1,95 +1,22 @@ -import { createAction, Property } from '@activepieces/pieces-framework'; -import { HttpMethod } from '@activepieces/pieces-common'; -import { oroAuth, oroApiCall } from '../common'; +import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { oroAuth, getOroBaseUrl, getAccessToken } from '../common'; -export const apiCallAction = createAction({ +export const customApiCallAction = createCustomApiCallAction({ auth: oroAuth, - name: 'api_call', - displayName: 'API Call', + name: 'custom_api_call', + displayName: 'Custom API Call', description: 'Make a direct authenticated call to the OroCommerce JSON:API.', + baseUrl: (auth) => auth ? getOroBaseUrl({ auth }) : '', + authMapping: async (auth) => ({ + Authorization: `Bearer ${await getAccessToken({ auth })}`, + 'Content-Type': 'application/vnd.api+json', + }), props: { - method: Property.StaticDropdown({ - displayName: 'Method', - required: true, - defaultValue: 'GET', - options: { - options: [ - { label: 'GET', value: 'GET' }, - { label: 'POST', value: 'POST' }, - { label: 'PATCH', value: 'PATCH' }, - { label: 'DELETE', value: 'DELETE' }, - ], - }, - }), - resourceUri: Property.ShortText({ - displayName: 'Resource URI', - description: - 'Path relative to the API base, e.g. orders, orders/42, invoices?filter[status]=open', - required: true, - }), - headers: Property.Json({ - displayName: 'Headers', - description: 'Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.', - required: false, + headers: { defaultValue: { 'Accept': 'application/vnd.api+json', 'X-Include': 'noHateoas;totalCount', }, - }), - queryParams: Property.Json({ - displayName: 'Query Parameters', - description: 'Additional query parameters as a JSON object, e.g. {"filter[status]":"open","page[size]":"20"}', - required: false, - defaultValue: {}, - }), - body: Property.Json({ - displayName: 'Request Body', - description: 'Request body as a JSON object (used for POST / PATCH).', - required: false, - defaultValue: {}, - }), - }, - - async run(context) { - const { method, resourceUri, headers, queryParams, body } = context.propsValue; - - // Build headers — strip empty/null values - const hdrs: Record = {}; - for (const [k, v] of Object.entries( - (headers as Record) ?? {} - )) { - if (v != null && String(v).trim() !== '') { - hdrs[k] = String(v); - } - } - - // Build query params — strip empty/null values - const qp: Record = {}; - for (const [k, v] of Object.entries( - (queryParams as Record) ?? {} - )) { - if (v != null && String(v).trim() !== '') { - qp[k] = String(v); - } - } - - const hasBody = - body != null && - typeof body === 'object' && - Object.keys(body as object).length > 0; - - const response = await oroApiCall({ - method: method as HttpMethod, - resourceUri, - auth: context.auth, - headers: Object.keys(hdrs).length > 0 ? hdrs : undefined, - queryParams: Object.keys(qp).length > 0 ? qp : undefined, - body: hasBody ? body : undefined, - }); - - return { - status: response.status, - body: response.body, - }; + }, }, }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/index.ts b/packages/pieces/community/orocommerce/src/lib/actions/index.ts index 951167afc45b..702abc29760d 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/index.ts @@ -1,5 +1,5 @@ export { createInvoiceAction } from './create-invoice'; export { createOrderAction } from './create-order'; -export { apiCallAction } from './api-call'; +export { customApiCallAction } from './api-call'; export { serializeJsonApiAction } from './serialize-jsonapi'; export { unserializeJsonApiAction } from './unserialize-jsonapi'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index 5192f3c885b6..0d29d4441c1b 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -37,7 +37,13 @@ function formatError({ error }: { error: unknown }): string { return `OroCommerce API Error: ${String(error)}`; } -async function getAccessToken({ auth }: { auth: OroAuth }): Promise { +export function getOroBaseUrl({ auth }: { auth: OroAuth }): string { + const serverUrl = auth.props.serverUrl.replace(/\/*$/, ''); + const adminPrefix = auth.props.adminPrefix.replace(/^\/+|\/+$/g, ''); + return `${serverUrl}/${adminPrefix}/api`; +} + +export async function getAccessToken({ auth }: { auth: OroAuth }): Promise { const cacheKey = buildCacheKey({ auth }); const cached = tokenCache.get(cacheKey); @@ -77,13 +83,11 @@ export async function oroApiCall({ headers: extraHeaders, }: OroApiCallParams): Promise> { try { - const serverUrl = auth.props.serverUrl.replace(/\/*$/, ''); - const adminPrefix = auth.props.adminPrefix.replace(/^\/+|\/+$/g, ''); const resource = resourceUri.replace(/^\/+/, ''); return await httpClient.sendRequest({ method, - url: `${serverUrl}/${adminPrefix}/api/${resource}`, + url: `${getOroBaseUrl({ auth })}/${resource}`, headers: { 'Content-Type': 'application/vnd.api+json', ...extraHeaders, From cc970276cb0522172db42f34888d15d506683b14 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 13 May 2026 19:11:20 +0200 Subject: [PATCH 20/50] feat(orocommerce): added invoice pdf support to action --- .../src/lib/actions/create-invoice.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index 533c6b9bf9b5..2a63ca757361 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -102,6 +102,18 @@ export const createInvoiceAction = createAction({ description: 'URL for the external payment page.', required: false, }), + invoicePdfContent: Property.LongText({ + displayName: 'Invoice PDF (Base64)', + description: + 'Base64-encoded PDF file content. When provided, the file is attached to the invoice as the default PDF.', + required: false, + }), + invoicePdfFilename: Property.ShortText({ + displayName: 'Invoice PDF Filename', + description: 'Filename for the attached PDF (e.g. invoice.pdf).', + required: false, + defaultValue: 'invoice.pdf', + }), organization: organizationDropdown, owner: userDropdown, @@ -262,6 +274,21 @@ export const createInvoiceAction = createAction({ }; } + if (p.invoicePdfContent) { + included.push({ + type: 'files', + id: 'invoiceDefaultPdfFile', + attributes: { + mimeType: 'application/pdf', + originalFilename: p.invoicePdfFilename || 'invoice.pdf', + content: p.invoicePdfContent, + }, + }); + relationships['invoiceDefaultPdfFile'] = { + data: { type: 'files', id: 'invoiceDefaultPdfFile' }, + }; + } + // -- POST /invoices ----------------------------------------------------- const response = await oroApiCall({ method: HttpMethod.POST, From f605946a240a7162c9d0c7d46f95c087e4c9cd69 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 21 May 2026 15:01:22 +0200 Subject: [PATCH 21/50] feat(orocommerce): icon update --- packages/pieces/community/orocommerce/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index fa95c9ffb828..e303c3d11275 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -14,8 +14,7 @@ export const orocommerce = createPiece({ displayName: 'OroCommerce', auth: oroAuth, minimumSupportedRelease: '0.36.1', - logoUrl: - 'https://oroinc.com/wp-content/themes/oroinc/images/redesign/ORO.svg', + logoUrl: 'https://static.oroinc.com/logo/O(logo).svg', categories: [PieceCategory.COMMERCE], description: 'B2B digital commerce solution', authors: ['Oro Inc.'], From 0bfd1702f69771949f27a7df509c9d045aa1f44f Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 27 May 2026 16:34:49 +0200 Subject: [PATCH 22/50] feat(orocommerce): added customer and customer user action, added extra fields --- .../pieces/community/orocommerce/src/index.ts | 8 + .../src/lib/actions/create-customer-user.ts | 175 +++++ .../src/lib/actions/create-customer.ts | 136 ++++ .../src/lib/actions/create-invoice.ts | 147 ++-- .../src/lib/actions/create-order.ts | 424 +++++----- .../orocommerce/src/lib/actions/index.ts | 4 + .../src/lib/actions/update-customer-user.ts | 147 ++++ .../src/lib/actions/update-customer.ts | 111 +++ .../src/lib/common/address-props.ts | 176 +++++ .../orocommerce/src/lib/common/index.ts | 1 + .../src/lib/common/jsonapi-body-utils.ts | 84 ++ .../src/lib/common/jsonapi/index.ts | 3 - .../orocommerce/src/lib/common/props.ts | 725 +++++++++--------- .../src/lib/triggers/webhook-topic-trigger.ts | 23 +- 14 files changed, 1475 insertions(+), 689 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/address-props.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index e303c3d11275..4aee28e89afc 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -5,6 +5,10 @@ import { oroWebhookTopicTrigger } from './lib/triggers/webhook-topic-trigger'; import { createInvoiceAction, createOrderAction, + createCustomerAction, + updateCustomerAction, + createCustomerUserAction, + updateCustomerUserAction, customApiCallAction, serializeJsonApiAction, unserializeJsonApiAction, @@ -21,6 +25,10 @@ export const orocommerce = createPiece({ actions: [ createInvoiceAction, createOrderAction, + createCustomerAction, + updateCustomerAction, + createCustomerUserAction, + updateCustomerUserAction, customApiCallAction, serializeJsonApiAction, unserializeJsonApiAction, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts new file mode 100644 index 000000000000..0851acf15199 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -0,0 +1,175 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerRequiredDropdown, + customerUserRoleDropdown, + organizationDropdown, + userDropdown, + websiteDropdown, + baseAddressArrayItemProps, + addressTypeProps, + buildIncludedAddress, + additionalAttributesProp, + additionalRelationsProp, +} from '../common'; +import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; + +export const createCustomerUserAction = createAction({ + auth: oroAuth, + name: 'create_customer_user', + displayName: 'Create Customer User', + description: + 'Creates a new customer user (storefront account) in OroCommerce.', + props: { + email: Property.ShortText({ + displayName: 'Email', + description: 'Email address of the customer user. Used as the login.', + required: true, + }), + password: Property.ShortText({ + displayName: 'Password', + description: 'Password for the new account. Must comply with the system security policy.', + required: true, + }), + + namePrefix: Property.ShortText({ + displayName: 'Name Prefix', + description: 'Honorific (e.g. Mr., Ms., Dr.).', + required: false, + }), + firstName: Property.ShortText({ + displayName: 'First Name', + required: true, + }), + middleName: Property.ShortText({ + displayName: 'Middle Name', + required: false, + }), + lastName: Property.ShortText({ + displayName: 'Last Name', + required: true, + }), + nameSuffix: Property.ShortText({ + displayName: 'Name Suffix', + description: 'Suffix (e.g. PhD, Jr.).', + required: false, + }), + + // -- Required relationships ------------------------------------------------ + customer: customerRequiredDropdown, + website: websiteDropdown, + + // -- Optional attributes --------------------------------------------------- + enabled: Property.Checkbox({ + displayName: 'Enabled', + description: 'When disabled the user cannot log into the storefront. Defaults to true.', + required: false, + defaultValue: true, + }), + confirmed: Property.Checkbox({ + displayName: 'Confirmed', + description: 'Whether the user has completed email confirmation. Defaults to true.', + required: false, + defaultValue: true, + }), + birthday: Property.ShortText({ + displayName: 'Birthday', + description: 'Birth date in YYYY-MM-DD format.', + required: false, + }), + externalId: Property.ShortText({ + displayName: 'External ID', + description: 'A unique identifier from an external system.', + required: false, + }), + + // -- Optional relationships ------------------------------------------------ + userRoles: customerUserRoleDropdown, + owner: userDropdown, + organization: organizationDropdown, + + // -- Addresses ------------------------------------------------------------- + addresses: Property.Array({ + displayName: 'Addresses', + description: 'Customer user addresses to create along with the user.', + required: false, + properties: { + ...baseAddressArrayItemProps, + primary: Property.Checkbox({ + displayName: 'Primary', + description: 'Mark this as the primary address.', + required: false, + }), + ...addressTypeProps, + }, + }), + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + }, + + async run(context) { + const p = context.propsValue; + + const included: Record[] = []; + + const rawAddresses = (p.addresses ?? []) as Array>; + const addressRelData = rawAddresses.map((addr, index) => { + const lid = `cu_addr_${index + 1}`; + included.push( + buildIncludedAddress({ lid, type: 'customeruseraddresses', addr }) + ); + return { type: 'customeruseraddresses', id: lid }; + }); + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { + email: p.email, + firstName: p.firstName, + lastName: p.lastName, + password: p.password, + enabled: p.enabled ?? true, + confirmed: p.confirmed ?? true, + ...jsonApiBodyUtils.pickDefined({ + namePrefix: p.namePrefix, + middleName: p.middleName, + nameSuffix: p.nameSuffix, + birthday: p.birthday, + externalId: p.externalId, + }), + ...extraAttrs, + }; + + const relationships = { + customer: { data: { type: 'customers', id: p.customer ?? '' } }, + ...jsonApiBodyUtils.buildRels({ + website: ['websites', p.website], + userRoles: ['customeruserroles', p.userRoles, true], + owner: ['users', p.owner], + organization: ['organizations', p.organization], + }), + ...(addressRelData.length > 0 + ? { addresses: { data: addressRelData } } + : {}), + ...extraRels, + }; + + const body: Record = { + data: { type: 'customerusers', attributes, relationships }, + }; + if (included.length > 0) body['included'] = included; + + const response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: '/customerusers', + auth: context.auth as OroAuth, + body, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts new file mode 100644 index 000000000000..e49dc94cb4ee --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts @@ -0,0 +1,136 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerDropdown, + customerGroupDropdown, + customerTaxCodeDropdown, + customerRatingDropdown, + organizationDropdown, + userDropdown, + paymentTermDropdown, + baseAddressArrayItemProps, + addressTypeProps, + buildIncludedAddress, + additionalAttributesProp, + additionalRelationsProp, +} from '../common'; +import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; + +export const createCustomerAction = createAction({ + auth: oroAuth, + name: 'create_customer', + displayName: 'Create Customer', + description: 'Creates a new customer (company) record in OroCommerce.', + props: { + // -- Required attributes --------------------------------------------------- + name: Property.ShortText({ + displayName: 'Name', + description: + 'A human-readable name that identifies the customer (company).', + required: true, + }), + + // -- Optional attributes --------------------------------------------------- + externalId: Property.ShortText({ + displayName: 'External ID', + description: 'A unique identifier from an external system.', + required: false, + }), + vat_id: Property.ShortText({ + displayName: 'VAT ID', + description: "Customer's value added tax identification number.", + required: false, + }), + + // -- Optional relationships ------------------------------------------------ + parent: { + ...customerDropdown, + displayName: 'Parent Customer', + description: 'The parent company this customer (division) reports to.', + }, + group: customerGroupDropdown, + taxCode: customerTaxCodeDropdown, + internalRating: customerRatingDropdown, + owner: userDropdown, + organization: organizationDropdown, + paymentTerm: paymentTermDropdown, + + // -- Addresses ------------------------------------------------------------- + addresses: Property.Array({ + displayName: 'Addresses', + description: 'Customer addresses to create along with the customer.', + required: false, + properties: { + ...baseAddressArrayItemProps, + primary: Property.Checkbox({ + displayName: 'Primary', + description: 'Mark this address as the primary customer address.', + required: false, + }), + ...addressTypeProps, + }, + }), + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + }, + + async run(context) { + const p = context.propsValue; + + const included: Record[] = []; + + const rawAddresses = (p.addresses ?? []) as Array>; + const addressRelData = rawAddresses.map((addr, index) => { + const lid = `addr_${index + 1}`; + included.push( + buildIncludedAddress({ lid, type: 'customeraddresses', addr }) + ); + return { type: 'customeraddresses', id: lid }; + }); + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { + name: p.name, + ...jsonApiBodyUtils.pickDefined({ + externalId: p.externalId, + vat_id: p.vat_id, + }), + ...extraAttrs, + }; + + const relationships = { + ...jsonApiBodyUtils.buildRels({ + parent: ['customers', p.parent], + group: ['customergroups', p.group], + taxCode: ['customertaxcodes', p.taxCode], + internal_rating: ['customerratings', p.internalRating], + owner: ['users', p.owner], + organization: ['organizations', p.organization], + paymentTerm: ['paymentterms', p.paymentTerm], + }), + ...(addressRelData.length > 0 + ? { addresses: { data: addressRelData } } + : {}), + ...extraRels, + }; + + const body: Record = { + data: { type: 'customers', attributes, relationships }, + }; + if (included.length > 0) body['included'] = included; + + const response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: '/customers', + auth: context.auth as OroAuth, + body, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index 2a63ca757361..1629359b062b 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -9,7 +9,10 @@ import { organizationDropdown, userDropdown, websiteDropdown, + additionalAttributesProp, + additionalRelationsProp, } from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; export const createInvoiceAction = createAction({ auth: oroAuth, @@ -177,20 +180,22 @@ export const createInvoiceAction = createAction({ }; }, }), + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, }, async run(context) { const p = context.propsValue; - // -- Build included line items with string IDs -------------------------- - // DynamicProperties wraps the array in an object keyed by "lineItems" const dynamicValue = (p.lineItems ?? {}) as Record; const rawItems = (dynamicValue['lineItems'] ?? []) as Array< Record >; - const included = rawItems.map((item, index) => { - const attrs: Record = { + const lineItemResources = rawItems.map((item, index) => ({ + type: 'invoicelineitems', + id: `li_${index + 1}`, + attributes: { position: index + 1, lineNumber: item['lineNumber'] || String(index + 1), description: item['description'], @@ -198,108 +203,76 @@ export const createInvoiceAction = createAction({ unitOfQuantity: item['unitOfQuantity'], unitPrice: Number(item['unitPrice']), rowTotal: Number(item['rowTotal']), - }; - if (item['note']) attrs['note'] = item['note']; - - return { - type: 'invoicelineitems', - id: `li_${index + 1}`, - attributes: attrs, - }; - }); + ...jsonApiBodyUtils.pickDefined({ + note: item['note'] as string | undefined, + }), + }, + })); - const lineItemsRelData = included.map((li) => ({ + const lineItemsRelData = lineItemResources.map((li) => ({ type: 'invoicelineitems', id: li.id, })); - // -- Build attributes --------------------------------------------------- - const attributes: Record = { + const pdfFile = p.invoicePdfContent + ? { + type: 'files', + id: 'invoiceDefaultPdfFile', + attributes: { + mimeType: 'application/pdf', + originalFilename: p.invoicePdfFilename || 'invoice.pdf', + content: p.invoicePdfContent, + }, + } + : undefined; + + const included = [...lineItemResources, ...(pdfFile ? [pdfFile] : [])]; + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { invoiceDate: p.invoiceDate, currency: p.currency, customerName: p.customerName, totalAmount: p.totalAmount, + ...jsonApiBodyUtils.pickDefined({ + invoiceNumber: p.invoiceNumber, + title: p.title, + description: p.description, + memo: p.memo, + billTo: p.billTo, + shipTo: p.shipTo, + shippingMethod: p.shippingMethod, + sellerInfo: p.sellerInfo, + externalPaymentUrl: p.externalPaymentUrl, + }), + ...extraAttrs, }; - if (p.invoiceNumber) attributes['invoiceNumber'] = p.invoiceNumber; - if (p.title) attributes['title'] = p.title; - if (p.description) attributes['description'] = p.description; - if (p.memo) attributes['memo'] = p.memo; - if (p.billTo) attributes['billTo'] = p.billTo; - if (p.shipTo) attributes['shipTo'] = p.shipTo; - if (p.shippingMethod) attributes['shippingMethod'] = p.shippingMethod; - if (p.sellerInfo) attributes['sellerInfo'] = p.sellerInfo; - if (p.externalPaymentUrl) - attributes['externalPaymentUrl'] = p.externalPaymentUrl; - // -- Build relationships ------------------------------------------------ - const relationships: Record = { + const relationships = { lineItems: { data: lineItemsRelData }, + ...jsonApiBodyUtils.buildRels({ + customer: ['customers', p.customer], + customer_user: ['customerusers', p.customerUser], + organization: ['organizations', p.organization], + owner: ['users', p.owner], + website: ['websites', p.website], + internal_status: ['invoiceinternalstatuses', p.internalStatus], + invoiceDefaultPdfFile: [ + 'files', + pdfFile ? 'invoiceDefaultPdfFile' : undefined, + ], + }), + ...extraRels, }; - const customerId = p.customer; - if (customerId) { - relationships['customer'] = { - data: { type: 'customers', id: customerId }, - }; - } - - const customerUserId = p.customerUser; - if (customerUserId) { - relationships['customer_user'] = { - data: { type: 'customerusers', id: customerUserId }, - }; - } - - const organizationId = p.organization; - if (organizationId) { - relationships['organization'] = { - data: { type: 'organizations', id: organizationId }, - }; - } - - const ownerId = p.owner; - if (ownerId) { - relationships['owner'] = { data: { type: 'users', id: ownerId } }; - } - - const websiteId = p.website; - if (websiteId) { - relationships['website'] = { data: { type: 'websites', id: websiteId } }; - } - - const internalStatusId = p.internalStatus; - if (internalStatusId) { - relationships['internal_status'] = { - data: { type: 'invoiceinternalstatuses', id: internalStatusId }, - }; - } - - if (p.invoicePdfContent) { - included.push({ - type: 'files', - id: 'invoiceDefaultPdfFile', - attributes: { - mimeType: 'application/pdf', - originalFilename: p.invoicePdfFilename || 'invoice.pdf', - content: p.invoicePdfContent, - }, - }); - relationships['invoiceDefaultPdfFile'] = { - data: { type: 'files', id: 'invoiceDefaultPdfFile' }, - }; - } - - // -- POST /invoices ----------------------------------------------------- const response = await oroApiCall({ method: HttpMethod.POST, resourceUri: '/invoices', auth: context.auth, body: { - data: { - type: 'invoices', - attributes, - relationships, - }, + data: { type: 'invoices', attributes, relationships }, included, }, }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index a6eb4c634542..14600677e876 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -16,8 +16,12 @@ import { buildCountryDropdown, buildRegionDropdown, productDropdown, + buildIncludedAddress, + additionalAttributesProp, + additionalRelationsProp, } from '../common'; import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; export const createOrderAction = createAction({ auth: oroAuth, @@ -130,12 +134,12 @@ export const createOrderAction = createAction({ displayName: 'Billing: Postal Code', required: false, }), - billingAddressCountry: buildCountryDropdown(false, 'Billing: Country'), - billingAddressRegion: buildRegionDropdown( - 'billingAddressCountry', - false, - 'Billing: Region / State' - ), + billingAddressCountry: buildCountryDropdown({ required: false, displayName: 'Billing: Country' }), + billingAddressRegion: buildRegionDropdown({ + countryRefresher: 'billingAddressCountry', + required: false, + displayName: 'Billing: Region / State', + }), billingAddressCustomRegion: Property.ShortText({ displayName: 'Billing: Custom Region', description: 'Free-text region for countries without predefined regions.', @@ -180,12 +184,12 @@ export const createOrderAction = createAction({ displayName: 'Shipping: Postal Code', required: false, }), - shippingAddressCountry: buildCountryDropdown(false, 'Shipping: Country'), - shippingAddressRegion: buildRegionDropdown( - 'shippingAddressCountry', - false, - 'Shipping: Region / State' - ), + shippingAddressCountry: buildCountryDropdown({ required: false, displayName: 'Shipping: Country' }), + shippingAddressRegion: buildRegionDropdown({ + countryRefresher: 'shippingAddressCountry', + required: false, + displayName: 'Shipping: Region / State', + }), shippingAddressCustomRegion: Property.ShortText({ displayName: 'Shipping: Custom Region', description: 'Free-text region for countries without predefined regions.', @@ -202,7 +206,10 @@ export const createOrderAction = createAction({ required: true, refreshers: [], props: async ({ auth }) => { - type JsonApiCollection = { data: { id: string; attributes: Record }[] }; + type JsonApiCollection = { + data: { id: string; attributes: Record }[]; + }; + const unitOptions: { label: string; value: string }[] = []; const warehouseOptions: { label: string; value: string }[] = []; @@ -220,14 +227,19 @@ export const createOrderAction = createAction({ value: item.id, }); } - } catch { /* leave empty */ } + } catch { + // Silently degrade so the form renders even if the catalog API is unavailable + } try { const warehousesResp = await oroApiCall({ method: HttpMethod.GET, resourceUri: '/warehouses', auth: auth as OroAuth, - queryParams: { 'page[size]': '100', 'fields[warehouses]': 'id,name' }, + queryParams: { + 'page[size]': '100', + 'fields[warehouses]': 'id,name', + }, }); for (const item of (warehousesResp.body as JsonApiCollection).data ?? []) { warehouseOptions.push({ @@ -235,7 +247,9 @@ export const createOrderAction = createAction({ value: item.id, }); } - } catch { /* leave empty */ } + } catch { + // Silently degrade so the form renders even if the catalog API is unavailable + } } return { @@ -340,220 +354,108 @@ export const createOrderAction = createAction({ // -- Product Search helper (top-level, search-enabled) --------------------- // Use this to find a product SKU/ID, then paste it into line item "Product SKU or ID". productSearch: productDropdown, + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, }, async run(context) { const p = context.propsValue; - const included: Record[] = []; - const relationships: Record = {}; - - // -- Helper: build an address included resource -------------------------- - function buildAddress( - localId: string, + const billingAddressResource = buildAddressResource({ + localId: 'billing_address', fields: { - label?: string | null; - firstName?: string | null; - lastName?: string | null; - organization?: string | null; - phone?: string | null; - street?: string | null; - street2?: string | null; - city?: string | null; - postalCode?: string | null; - country?: string | null; - region?: string | null; - customRegion?: string | null; - } - ): boolean { - const hasData = Object.values(fields).some((v) => v != null && v !== ''); - if (!hasData) return false; - - const attrs: Record = { fromExternalSource: false }; - const rels: Record = {}; - - const textFields: Array = [ - 'label', 'firstName', 'lastName', 'organization', - 'phone', 'street', 'street2', 'city', 'postalCode', 'customRegion', - ]; - for (const f of textFields) { - if (fields[f]) attrs[f] = fields[f]; - } - if (fields.country) { - rels['country'] = { data: { type: 'countries', id: fields.country } }; - } - if (fields.region) { - rels['region'] = { data: { type: 'regions', id: fields.region } }; - } - - included.push({ - type: 'orderaddresses', - id: localId, - attributes: attrs, - relationships: rels, - }); - return true; - } - - // -- Billing address ----------------------------------------------------- - const billingAdded = buildAddress('billing_address', { - label: p.billingAddressLabel, - firstName: p.billingAddressFirstName, - lastName: p.billingAddressLastName, - organization: p.billingAddressOrganization, - phone: p.billingAddressPhone, - street: p.billingAddressStreet, - street2: p.billingAddressStreet2, - city: p.billingAddressCity, - postalCode: p.billingAddressPostalCode, - country: p.billingAddressCountry, - region: p.billingAddressRegion, - customRegion: p.billingAddressCustomRegion, + label: p.billingAddressLabel, + firstName: p.billingAddressFirstName, + lastName: p.billingAddressLastName, + organization: p.billingAddressOrganization, + phone: p.billingAddressPhone, + street: p.billingAddressStreet, + street2: p.billingAddressStreet2, + city: p.billingAddressCity, + postalCode: p.billingAddressPostalCode, + country: p.billingAddressCountry, + region: p.billingAddressRegion, + customRegion: p.billingAddressCustomRegion, + }, }); - if (billingAdded) { - relationships['billingAddress'] = { - data: { type: 'orderaddresses', id: 'billing_address' }, - }; - } - - // -- Shipping address ---------------------------------------------------- - const shippingAdded = buildAddress('shipping_address', { - label: p.shippingAddressLabel, - firstName: p.shippingAddressFirstName, - lastName: p.shippingAddressLastName, - organization: p.shippingAddressOrganization, - phone: p.shippingAddressPhone, - street: p.shippingAddressStreet, - street2: p.shippingAddressStreet2, - city: p.shippingAddressCity, - postalCode: p.shippingAddressPostalCode, - country: p.shippingAddressCountry, - region: p.shippingAddressRegion, - customRegion: p.shippingAddressCustomRegion, + + const shippingAddressResource = buildAddressResource({ + localId: 'shipping_address', + fields: { + label: p.shippingAddressLabel, + firstName: p.shippingAddressFirstName, + lastName: p.shippingAddressLastName, + organization: p.shippingAddressOrganization, + phone: p.shippingAddressPhone, + street: p.shippingAddressStreet, + street2: p.shippingAddressStreet2, + city: p.shippingAddressCity, + postalCode: p.shippingAddressPostalCode, + country: p.shippingAddressCountry, + region: p.shippingAddressRegion, + customRegion: p.shippingAddressCustomRegion, + }, }); - if (shippingAdded) { - relationships['shippingAddress'] = { - data: { type: 'orderaddresses', id: 'shipping_address' }, - }; - } // -- Line items ---------------------------------------------------------- // DynamicProperties wraps the array in an object keyed by "lineItems" const dynamicValue = (p.lineItems ?? {}) as Record; const rawItems = (dynamicValue['lineItems'] ?? []) as Array>; - const lineItemsRelData = rawItems.map((item, index) => { - const lid = `li_${index + 1}`; - - const productUnitId = item['productUnit'] as string | undefined; - - const liAttributes: Record = { - productSku: String(item['productSku'] ?? ''), - quantity: Number(item['quantity']), - value: Number(item['value']), - currency: item['currency'] || p.currency || 'USD', - priceType: item['priceType'] != null ? Number(item['priceType']) : 10, - fromExternalSource: true, - }; - if (item['productName']) liAttributes['productName'] = item['productName']; - if (item['freeFormProduct']) liAttributes['freeFormProduct'] = item['freeFormProduct']; - if (item['comment']) liAttributes['comment'] = item['comment']; - if (item['shipBy']) liAttributes['shipBy'] = item['shipBy']; - if (item['shippingEstimateAmount'] != null) liAttributes['shippingEstimateAmount'] = Number(item['shippingEstimateAmount']); - if (item['shippingMethod']) liAttributes['shippingMethod'] = item['shippingMethod']; - if (item['shippingMethodType']) liAttributes['shippingMethodType'] = item['shippingMethodType']; - - const liRelationships: Record = { - productUnit: { data: { type: 'productunits', id: String(productUnitId) } }, - }; - - const productId = (item['productId'] as string | undefined)?.trim(); - if (productId) { - liRelationships['product'] = { data: { type: 'products', id: productId } }; - } - - const liWarehouseId = (item['warehouseId'] as string | undefined)?.trim() - || (item['warehouse'] as string | undefined)?.trim(); - if (liWarehouseId) { - liRelationships['warehouse'] = { data: { type: 'warehouses', id: liWarehouseId } }; - } - - included.push({ - type: 'orderlineitems', - id: lid, - attributes: liAttributes, - relationships: liRelationships, - }); - - return { type: 'orderlineitems', id: lid }; - }); - relationships['lineItems'] = { data: lineItemsRelData }; + const lineItems = rawItems.map((item, index) => buildOrderLineItem({ item, index, orderCurrency: p.currency })); + const lineItemResources = lineItems.map((li) => li.resource); + const lineItemsRelData = lineItems.map((li) => li.ref); + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); // -- Order attributes ---------------------------------------------------- - const attributes: Record = { external: false }; - if (p.currency) attributes['currency'] = p.currency; - if (p.identifier) attributes['identifier'] = p.identifier; - if (p.poNumber) attributes['poNumber'] = p.poNumber; - if (p.customerNotes) attributes['customerNotes'] = p.customerNotes; - if (p.shipUntil) attributes['shipUntil'] = p.shipUntil; - if (p.overriddenShippingCostAmount != null) attributes['overriddenShippingCostAmount'] = p.overriddenShippingCostAmount; - if (p.estimatedShippingCostAmount != null) attributes['estimatedShippingCostAmount'] = p.estimatedShippingCostAmount; - if (p.shippingMethod) attributes['shippingMethod'] = p.shippingMethod; - if (p.shippingMethodType) attributes['shippingMethodType'] = p.shippingMethodType; - if (p.disablePromotions) attributes['disablePromotions'] = p.disablePromotions; - - // -- Order relationships -- - const customerId = p.customer; - if (customerId) { - relationships['customer'] = { data: { type: 'customers', id: customerId } }; - } - - const customerUserId = p.customerUser; - if (customerUserId) { - relationships['customerUser'] = { data: { type: 'customerusers', id: customerUserId } }; - } - - const organizationId = p.organization; - if (organizationId) { - relationships['organization'] = { data: { type: 'organizations', id: organizationId } }; - } - - const ownerId = p.owner; - if (ownerId) { - relationships['owner'] = { data: { type: 'users', id: ownerId } }; - } - - const websiteId = p.website; - if (websiteId) { - relationships['website'] = { data: { type: 'websites', id: websiteId } }; - } - - const internalStatusId = p.internalStatus; - if (internalStatusId) { - relationships['internalStatus'] = { data: { type: 'orderinternalstatuses', id: internalStatusId } }; - } - - const paymentTermId = p.paymentTerm; - if (paymentTermId) { - relationships['paymentTerm'] = { data: { type: 'paymentterms', id: paymentTermId } }; - } - - const warehouseId = p.warehouse; - if (warehouseId) { - relationships['warehouse'] = { data: { type: 'warehouses', id: warehouseId } }; - } - - const parentId = p.parent; - if (parentId) { - relationships['parent'] = { data: { type: 'orders', id: parentId } }; - } - - const statusId = p.status; - if (statusId) { - relationships['status'] = { data: { type: 'orderstatuses', id: statusId } }; - } - - // -- POST /orders --------------------------------------------------------- + const attributes: Record = { + external: false, + ...jsonApiBodyUtils.pickDefined({ + currency: p.currency, + identifier: p.identifier, + poNumber: p.poNumber, + customerNotes: p.customerNotes, + shipUntil: p.shipUntil, + overriddenShippingCostAmount: p.overriddenShippingCostAmount, + estimatedShippingCostAmount: p.estimatedShippingCostAmount, + shippingMethod: p.shippingMethod, + shippingMethodType: p.shippingMethodType, + disablePromotions: p.disablePromotions, + }), + ...extraAttrs, + }; + + const relationships: Record = { + lineItems: { data: lineItemsRelData }, + ...(billingAddressResource + ? { billingAddress: { data: { type: 'orderaddresses', id: 'billing_address' } } } + : {}), + ...(shippingAddressResource + ? { shippingAddress: { data: { type: 'orderaddresses', id: 'shipping_address' } } } + : {}), + ...jsonApiBodyUtils.buildRels({ + customer: ['customers', p.customer], + customerUser: ['customerusers', p.customerUser], + organization: ['organizations', p.organization], + owner: ['users', p.owner], + website: ['websites', p.website], + internalStatus: ['orderinternalstatuses', p.internalStatus], + paymentTerm: ['paymentterms', p.paymentTerm], + warehouse: ['warehouses', p.warehouse], + parent: ['orders', p.parent], + status: ['orderstatuses', p.status], + }), + ...extraRels, + }; + + const included = [ + ...lineItemResources, + ...(billingAddressResource ? [billingAddressResource] : []), + ...(shippingAddressResource ? [shippingAddressResource] : []), + ]; + const response = await oroApiCall({ method: HttpMethod.POST, resourceUri: '/orders', @@ -567,3 +469,97 @@ export const createOrderAction = createAction({ return response.body; }, }); + +function buildAddressResource({ + localId, + fields, +}: { + localId: string; + fields: { + label?: string | null; + namePrefix?: string | null; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + nameSuffix?: string | null; + organization?: string | null; + phone?: string | null; + street?: string | null; + street2?: string | null; + city?: string | null; + postalCode?: string | null; + country?: string | null; + region?: string | null; + customRegion?: string | null; + }; +}): Record | null { + const hasData = Object.values(fields).some((v) => v != null && v !== ''); + if (!hasData) return null; + + return buildIncludedAddress({ + lid: localId, + type: 'orderaddresses', + addr: fields as Record, + extraAttributes: { fromExternalSource: false }, + }); +} + +function buildOrderLineItem({ + item, + index, + orderCurrency, +}: { + item: Record; + index: number; + orderCurrency: string | null | undefined; +}): { resource: Record; ref: { type: string; id: string } } { + const lid = `li_${index + 1}`; + const productUnitId = item['productUnit'] as string | undefined; + + const attributes: Record = { + fromExternalSource: true, + productSku: String(item['productSku'] ?? ''), + quantity: Number(item['quantity']), + value: Number(item['value']), + currency: + (item['currency'] as string | undefined) || orderCurrency || 'USD', + priceType: item['priceType'] != null ? Number(item['priceType']) : 10, + + ...jsonApiBodyUtils.pickDefined({ + productName: item['productName'] as string | undefined, + freeFormProduct: item['freeFormProduct'] as string | undefined, + comment: item['comment'] as string | undefined, + shipBy: item['shipBy'] as string | undefined, + }), + + ...(item['shippingEstimateAmount'] != null + ? { shippingEstimateAmount: Number(item['shippingEstimateAmount']) } + : {}), + + ...(item['shippingMethod'] + ? { shippingMethod: item['shippingMethod'] } + : {}), + + ...(item['shippingMethodType'] + ? { shippingMethodType: item['shippingMethodType'] } + : {}), + }; + + const productId = (item['productId'] as string | undefined)?.trim(); + const liWarehouseId = + (item['warehouseId'] as string | undefined)?.trim() || + (item['warehouse'] as string | undefined)?.trim(); + + const relationships: Record = { + productUnit: { data: { type: 'productunits', id: String(productUnitId) } }, + ...jsonApiBodyUtils.buildRels({ + product: ['products', productId], + warehouse: ['warehouses', liWarehouseId], + }), + }; + + return { + resource: { type: 'orderlineitems', id: lid, attributes, relationships }, + ref: { type: 'orderlineitems', id: lid }, + }; +} diff --git a/packages/pieces/community/orocommerce/src/lib/actions/index.ts b/packages/pieces/community/orocommerce/src/lib/actions/index.ts index 702abc29760d..1d436c1809ab 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/index.ts @@ -1,5 +1,9 @@ export { createInvoiceAction } from './create-invoice'; export { createOrderAction } from './create-order'; +export { createCustomerAction } from './create-customer'; +export { updateCustomerAction } from './update-customer'; +export { createCustomerUserAction } from './create-customer-user'; +export { updateCustomerUserAction } from './update-customer-user'; export { customApiCallAction } from './api-call'; export { serializeJsonApiAction } from './serialize-jsonapi'; export { unserializeJsonApiAction } from './unserialize-jsonapi'; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts new file mode 100644 index 000000000000..207e3a68b74d --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -0,0 +1,147 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerDropdown, + customerUserRoleDropdown, + organizationDropdown, + userDropdown, + websiteDropdown, + additionalAttributesProp, + additionalRelationsProp, +} from '../common'; +import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; + +export const updateCustomerUserAction = createAction({ + auth: oroAuth, + name: 'update_customer_user', + displayName: 'Update Customer User', + description: + 'Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.', + props: { + // -- Target record --------------------------------------------------------- + customerUserId: Property.ShortText({ + displayName: 'Customer User ID', + description: 'The numeric ID of the customer user to update.', + required: true, + }), + + // -- Attributes ------------------------------------------------------------ + email: Property.ShortText({ + displayName: 'Email', + description: 'Updated email address of the customer user.', + required: false, + }), + password: Property.ShortText({ + displayName: 'Password', + description: 'New password for the account. Must comply with the system security policy.', + required: false, + }), + + namePrefix: Property.ShortText({ + displayName: 'Name Prefix', + description: 'Honorific (e.g. Mr., Ms., Dr.).', + required: false, + }), + firstName: Property.ShortText({ + displayName: 'First Name', + required: false, + }), + middleName: Property.ShortText({ + displayName: 'Middle Name', + required: false, + }), + lastName: Property.ShortText({ + displayName: 'Last Name', + required: false, + }), + nameSuffix: Property.ShortText({ + displayName: 'Name Suffix', + description: 'Suffix (e.g. PhD, Jr.).', + required: false, + }), + + enabled: Property.Checkbox({ + displayName: 'Enabled', + description: 'Enable or disable the storefront account.', + required: false, + }), + confirmed: Property.Checkbox({ + displayName: 'Confirmed', + description: 'Whether the user has completed email confirmation.', + required: false, + }), + birthday: Property.ShortText({ + displayName: 'Birthday', + description: 'Birth date in YYYY-MM-DD format.', + required: false, + }), + externalId: Property.ShortText({ + displayName: 'External ID', + description: 'A unique identifier from an external system.', + required: false, + }), + + // -- Relationships --------------------------------------------------------- + customer: customerDropdown, + website: websiteDropdown, + userRoles: customerUserRoleDropdown, + owner: userDropdown, + organization: organizationDropdown, + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + }, + + async run(context) { + const p = context.propsValue; + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { + ...jsonApiBodyUtils.pickDefined({ + email: p.email, + firstName: p.firstName, + lastName: p.lastName, + password: p.password, + enabled: p.enabled ?? undefined, + confirmed: p.confirmed ?? undefined, + namePrefix: p.namePrefix, + middleName: p.middleName, + nameSuffix: p.nameSuffix, + birthday: p.birthday, + externalId: p.externalId, + }), + ...extraAttrs, + }; + + const relationships = { + ...jsonApiBodyUtils.buildRels({ + customer: ['customers', p.customer], + website: ['websites', p.website], + userRoles: ['customeruserroles', p.userRoles, true], + owner: ['users', p.owner], + organization: ['organizations', p.organization], + }), + ...extraRels, + }; + + const response = await oroApiCall({ + method: HttpMethod.PATCH, + resourceUri: `/customerusers/${p.customerUserId}`, + auth: context.auth as OroAuth, + body: { + data: jsonApiBodyUtils.omitEmptyObjects({ + type: 'customerusers', + id: p.customerUserId, + attributes, + relationships, + }), + }, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts new file mode 100644 index 000000000000..67c161c66cc4 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts @@ -0,0 +1,111 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerDropdown, + customerGroupDropdown, + customerTaxCodeDropdown, + customerRatingDropdown, + organizationDropdown, + userDropdown, + paymentTermDropdown, + additionalAttributesProp, + additionalRelationsProp, +} from '../common'; +import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; + +export const updateCustomerAction = createAction({ + auth: oroAuth, + name: 'update_customer', + displayName: 'Update Customer', + description: + 'Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.', + props: { + // -- Target record --------------------------------------------------------- + customerId: Property.ShortText({ + displayName: 'Customer ID', + description: 'The numeric ID of the customer to update.', + required: true, + }), + + // -- Attributes ------------------------------------------------------------ + name: Property.ShortText({ + displayName: 'Name', + description: + 'A human-readable name that identifies the customer (company).', + required: false, + }), + externalId: Property.ShortText({ + displayName: 'External ID', + description: 'A unique identifier from an external system.', + required: false, + }), + vat_id: Property.ShortText({ + displayName: 'VAT ID', + description: "Customer's value added tax identification number.", + required: false, + }), + + // -- Relationships --------------------------------------------------------- + parent: { + ...customerDropdown, + displayName: 'Parent Customer', + description: 'The parent company this customer (division) reports to.', + }, + group: customerGroupDropdown, + taxCode: customerTaxCodeDropdown, + internalRating: customerRatingDropdown, + owner: userDropdown, + organization: organizationDropdown, + paymentTerm: paymentTermDropdown, + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + }, + + async run(context) { + const p = context.propsValue; + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { + ...jsonApiBodyUtils.pickDefined({ + name: p.name, + externalId: p.externalId, + vat_id: p.vat_id, + }), + ...extraAttrs, + }; + + const relationships = { + ...jsonApiBodyUtils.buildRels({ + parent: ['customers', p.parent], + group: ['customergroups', p.group], + taxCode: ['customertaxcodes', p.taxCode], + internal_rating: ['customerratings', p.internalRating], + owner: ['users', p.owner], + organization: ['organizations', p.organization], + paymentTerm: ['paymentterms', p.paymentTerm], + }), + ...extraRels, + }; + + const response = await oroApiCall({ + method: HttpMethod.PATCH, + resourceUri: `/customers/${p.customerId}`, + auth: context.auth as OroAuth, + body: { + data: jsonApiBodyUtils.omitEmptyObjects({ + type: 'customers', + id: p.customerId, + attributes, + relationships, + }), + }, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/orocommerce/src/lib/common/address-props.ts b/packages/pieces/community/orocommerce/src/lib/common/address-props.ts new file mode 100644 index 000000000000..0740f97e26e6 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/address-props.ts @@ -0,0 +1,176 @@ +import { Property } from '@activepieces/pieces-framework'; + +export function buildIncludedAddress({ + lid, + type, + addr, + extraAttributes = {}, +}: { + lid: string; + type: string; + addr: Record; + extraAttributes?: Record; +}): Record { + const attrs: Record = {}; + const rels: Record = {}; + + const textFields = [ + 'label', + 'namePrefix', + 'firstName', + 'middleName', + 'lastName', + 'nameSuffix', + 'organization', + 'phone', + 'street', + 'street2', + 'city', + 'postalCode', + 'customRegion', + ]; + for (const f of textFields) { + if (addr[f]) attrs[f] = addr[f]; + } + if (addr['primary'] != null) attrs['primary'] = addr['primary']; + + const types = buildAddressTypes({ + typesBilling: addr['typesBilling'] as boolean | undefined, + typesShipping: addr['typesShipping'] as boolean | undefined, + defaultBilling: addr['defaultBilling'] as boolean | undefined, + defaultShipping: addr['defaultShipping'] as boolean | undefined, + }); + if (types.length > 0) attrs['types'] = types; + + if (addr['country']) { + rels['country'] = { data: { type: 'countries', id: addr['country'] } }; + } + if (addr['region']) { + rels['region'] = { data: { type: 'regions', id: addr['region'] } }; + } + + return { type, id: lid, attributes: { ...extraAttributes, ...attrs }, relationships: rels }; +} + +function buildAddressTypes({ + typesBilling, + typesShipping, + defaultBilling, + defaultShipping, +}: { + typesBilling?: boolean; + typesShipping?: boolean; + defaultBilling?: boolean; + defaultShipping?: boolean; +}): Array<{ addressType: string; default: boolean }> { + return [ + ...(typesBilling ? [{ addressType: 'billing', default: defaultBilling === true }] : []), + ...(typesShipping ? [{ addressType: 'shipping', default: defaultShipping === true }] : []), + ]; +} + +/** + * Core address fields for use inside `Property.Array`. + * Auth-bound dropdowns are not permitted in array item properties, so + * country and region accept raw ISO codes as ShortText. + * Spread into `Property.Array` `properties` and extend with entity-specific fields. + */ +export const baseAddressArrayItemProps = { + label: Property.ShortText({ + displayName: 'Label', + description: 'Human-readable identifier for this address (e.g. "Main Office").', + required: false, + }), + namePrefix: Property.ShortText({ + displayName: 'Name Prefix', + description: 'Honorific of the contact person (e.g. Mr., Dr.).', + required: false, + }), + firstName: Property.ShortText({ + displayName: 'First Name', + required: false, + }), + middleName: Property.ShortText({ + displayName: 'Middle Name', + required: false, + }), + lastName: Property.ShortText({ + displayName: 'Last Name', + required: false, + }), + nameSuffix: Property.ShortText({ + displayName: 'Name Suffix', + description: 'Name suffix (e.g. Jr., M.D.).', + required: false, + }), + organization: Property.ShortText({ + displayName: 'Organization', + description: 'Organisation the contact person belongs to.', + required: false, + }), + phone: Property.ShortText({ + displayName: 'Phone', + required: false, + }), + street: Property.ShortText({ + displayName: 'Street', + required: false, + }), + street2: Property.ShortText({ + displayName: 'Street 2', + required: false, + }), + city: Property.ShortText({ + displayName: 'City', + required: false, + }), + postalCode: Property.ShortText({ + displayName: 'Postal Code', + required: false, + }), + country: Property.ShortText({ + displayName: 'Country', + description: 'ISO-3166 two-letter country code (e.g. US, DE). Use the Custom API Call action to look up /countries.', + required: false, + }), + region: Property.ShortText({ + displayName: 'Region / State', + description: 'ISO 3166-2 region code (e.g. US-NY). Use the Custom API Call action to look up /regions.', + required: false, + }), + customRegion: Property.ShortText({ + displayName: 'Custom Region', + description: 'Free-text region for countries without predefined regions.', + required: false, + }), +}; + +/** + * Billing / shipping type fields. Spread after `baseAddressArrayItemProps` + * for entities that support OroCommerce's `types` attribute + * (customeraddresses, customeruseraddresses). + */ +export const addressTypeProps = { + typesBilling: Property.Checkbox({ + displayName: 'Billing Address', + description: 'Mark this address as a billing address.', + required: false, + }), + typesShipping: Property.Checkbox({ + displayName: 'Shipping Address', + description: 'Mark this address as a shipping address.', + required: false, + }), + defaultBilling: Property.Checkbox({ + displayName: 'Default for Billing', + description: 'Use as the default billing address.', + required: false, + }), + defaultShipping: Property.Checkbox({ + displayName: 'Default for Shipping', + description: 'Use as the default shipping address.', + required: false, + }), +}; + + diff --git a/packages/pieces/community/orocommerce/src/lib/common/index.ts b/packages/pieces/community/orocommerce/src/lib/common/index.ts index 8a442b37b9ad..f8e52694be43 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -1,3 +1,4 @@ export * from './auth'; export * from './client'; export * from './props'; +export * from './address-props'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts new file mode 100644 index 000000000000..e408f845721e --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts @@ -0,0 +1,84 @@ +function pickDefined>(obj: T): Partial { + return Object.fromEntries( + Object.entries(obj).filter(([, v]) => v != null) + ) as Partial; +} + +function omitEmptyObjects>( + obj: T +): Partial { + return Object.fromEntries( + Object.entries(obj).filter( + ([, v]) => + !( + v !== null && + typeof v === 'object' && + !Array.isArray(v) && + Object.keys(v).length === 0 + ) + ) + ) as Partial; +} + +function buildRels( + map: Record< + string, + [type: string, id: string | string[] | null | undefined, many?: boolean] + > +): Record { + return Object.fromEntries( + Object.entries(map) + .filter(([, [, id]]) => id != null && (!Array.isArray(id) || id.length > 0)) + .map(([key, [type, id, many]]) => [ + key, + Array.isArray(id) + ? { data: id.map((i) => ({ type, id: i })) } + : many + ? { data: [{ type, id }] } + : { data: { type, id } }, + ]) + ); +} + +function parseAdditionalAttributes( + raw: unknown +): Record { + if (raw == null) return {}; + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; + if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) { + throw new Error( + 'Additional Attributes must be a flat JSON object, e.g. {"myField": "value"}.' + ); + } + return parsed as Record; +} + +function parseAdditionalRelations( + raw: unknown +): Record { + if (raw == null) return {}; + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; + if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) { + throw new Error( + 'Additional Relations must be a JSON object, e.g. {"myRelation": {"data": {"type": "myentities", "id": "1"}}}.' + ); + } + const obj = parsed as Record; + for (const [key, value] of Object.entries(obj)) { + if (value === null) continue; + if (typeof value !== 'object') { + throw new Error( + `Additional Relations: "${key}" must be a JSON:API linkage object with a "data" key, e.g. {"data": {"type": "myentities", "id": "1"}}.` + ); + } + const linkage = value as Record; + if (!('data' in linkage)) { + throw new Error( + `Additional Relations: "${key}" is missing the "data" key. Expected format: {"data": {"type": "myentities", "id": "1"}}.` + ); + } + } + return obj; +} + +export const jsonApiBodyUtils = { pickDefined, omitEmptyObjects, buildRels, parseAdditionalAttributes, parseAdditionalRelations }; diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts index 90bf17f6bf00..1f6b874ee7d5 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts @@ -1,6 +1,3 @@ -/** - * This library is generated by Claude Sonnet 4.6 - */ export * from './types'; export * from './deserialize'; export * from './serialize'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index 12d1da893c36..70c78cd9b9c2 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -4,7 +4,7 @@ import { oroAuth } from './auth'; import { oroApiCall, fetchCollection } from './client'; import { OroAuth, OroJsonApiItem } from './types'; -// --- Helpers ------------------------------------------------------------------ +// --- Shared sentinels --------------------------------------------------------- const NOT_CONNECTED = { disabled: true, @@ -18,39 +18,113 @@ const FAILED = { options: [], }; -// --- Customers ---------------------------------------------------------------- -// Supports filter[searchText] — search across name and other indexed fields. +// --- Dropdown builders -------------------------------------------------------- + +function sanitizeSearch(value: string): string { + return value.trim().replaceAll('"', ''); +} + +function attrLabel(...attrs: string[]): LabelFn { + return (item) => + String( + attrs.reduce( + (v, attr) => v ?? item.attributes[attr], + undefined + ) ?? item.id + ); +} + +function clientSideFilterAndSort({ + items, + searchValue, + labelAttr, +}: { + items: OroJsonApiItem[]; + searchValue: string | undefined; + labelAttr: string; +}) { + const lower = searchValue?.toLowerCase() ?? ''; + return items + .map((item) => ({ + label: String(item.attributes[labelAttr] ?? item.id), + value: item.id, + })) + .filter(({ label }) => !lower || label.toLowerCase().includes(lower)) + .sort((a, b) => a.label.localeCompare(b.label)); +} -function buildCustomerOptions( - required: boolean, - displayName = 'Customer', - description = 'Select a customer.' -) { +function makeSearchableDropdown({ + displayName, + description, + required = false, + refreshers = [], + resourceUri, + fieldsParam, + searchExpr, + labelFn, + extraParams = {}, +}: SearchableDropdownConfig) { return Property.Dropdown({ auth: oroAuth, displayName, description, required, - refreshers: [], + refreshers, refreshOnSearch: true, options: async ({ auth }, { searchValue }) => { if (!auth) return NOT_CONNECTED; try { const params: Record = { - 'fields[customers]': 'id,name', + [`fields[${resourceUri.slice(1)}]`]: fieldsParam, + ...extraParams, }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; + const trimmed = searchValue?.trim() ?? ''; + if (trimmed.length > 0) { + params['filter[searchQuery]'] = searchExpr(sanitizeSearch(trimmed)); } - const items = await fetchCollection({ auth: auth as OroAuth, - resourceUri: '/customers', + resourceUri, queryParams: params, }); return { options: items.map((item) => ({ - label: String(item.attributes['name'] ?? item.id), + label: labelFn(item), + value: item.id, + })), + }; + } catch { + return FAILED; + } + }, + }); +} + +function makeEnumDropdown({ + displayName, + description, + required = false, + resourceUri, + labelFn, + extraParams = {}, +}: EnumDropdownConfig) { + return Property.Dropdown({ + auth: oroAuth, + displayName, + description, + required, + refreshers: [], + options: async ({ auth }) => { + if (!auth) return NOT_CONNECTED; + try { + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri, + queryParams: extraParams, + }); + return { + options: items.map((item) => ({ + label: labelFn(item), value: item.id, })), }; @@ -61,15 +135,31 @@ function buildCustomerOptions( }); } -export const customerDropdown = buildCustomerOptions(false); -export const customerRequiredDropdown = buildCustomerOptions( - true, - 'Customer', - 'The customer this order belongs to.' -); +// --- Customers ---------------------------------------------------------------- + +export const customerDropdown = makeSearchableDropdown({ + displayName: 'Customer', + description: 'Select a customer.', + required: false, + resourceUri: '/customers', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +export const customerRequiredDropdown = makeSearchableDropdown({ + displayName: 'Customer', + description: 'The customer this order belongs to.', + required: true, + resourceUri: '/customers', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); // --- Customer Users ----------------------------------------------------------- // Pre-filters by customer relationship when customer is selected. +// Kept custom due to multi-refresher logic and composite label. export const customerUserDropdown = (required = false) => Property.Dropdown({ @@ -81,14 +171,9 @@ export const customerUserDropdown = (required = false) => refreshOnSearch: true, options: async ({ auth, customer, customerId }, { searchValue }) => { if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - 'fields[customerusers]': 'id,firstName,lastName,email', - }; - const resolvedCustomer = (customerId as string) || (customer as string); - if (!resolvedCustomer || resolvedCustomer.length == 0) { + if (!resolvedCustomer || resolvedCustomer.length === 0) { return { disabled: true, placeholder: 'Select Customer first.', @@ -96,24 +181,25 @@ export const customerUserDropdown = (required = false) => }; } const searchFilters = [`customer_id = ${resolvedCustomer}`]; - if (searchValue && searchValue.trim().length > 0) { - searchFilters.push(`allText ~ "${searchValue.trim().replace('"', '')}"`); + const trimmed = searchValue?.trim() ?? ''; + if (trimmed.length > 0) { + searchFilters.push(`allText ~ "${sanitizeSearch(trimmed)}"`); } - - params['filter[searchQuery]'] = searchFilters.join(' and '); const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/customerusers', - queryParams: params, + queryParams: { + 'fields[customerusers]': 'id,firstName,lastName,email', + 'filter[searchQuery]': searchFilters.join(' and '), + }, }); return { options: items.map((item) => { const firstName = String(item.attributes['firstName'] ?? ''); const lastName = String(item.attributes['lastName'] ?? ''); const email = String(item.attributes['email'] ?? ''); - const label = - [firstName, lastName].filter(Boolean).join(' ') + ' - ' + email || - item.id; + const namePart = [firstName, lastName].filter(Boolean).join(' '); + const label = [namePart, email].filter(Boolean).join(' - ') || item.id; return { label, value: item.id }; }), }; @@ -125,165 +211,69 @@ export const customerUserDropdown = (required = false) => // --- Organizations ------------------------------------------------------------ -export const organizationDropdown = Property.Dropdown({ - auth: oroAuth, +export const organizationDropdown = makeSearchableDropdown({ displayName: 'Organization', - description: 'Search organizations.', - required: false, - refreshers: [], - refreshOnSearch: true, - options: async ({ auth }, { searchValue }) => { - if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - 'fields[organizations]': 'id,name' - }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; - } - - const items = await fetchCollection({ - auth: auth as OroAuth, - resourceUri: '/organizations', - queryParams: params, - }); - return { - options: items.map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + description: 'The organization this record belongs to.', + resourceUri: '/organizations', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), }); -// --- Owner (admin users) ------------------------------------------------------ - -export const userDropdown = Property.Dropdown({ - auth: oroAuth, - displayName: 'User', - description: 'Search user by name, username or email.', - required: false, - refreshers: [], - refreshOnSearch: true, - options: async ({ auth }, { searchValue }) => { - if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - 'fields[users]': 'id,firstName,lastName,username', - }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; - } - const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/users', queryParams: params }); - return { - options: items.map((item) => { - const firstName = String(item.attributes['firstName'] ?? ''); - const lastName = String(item.attributes['lastName'] ?? ''); - const username = String(item.attributes['username'] ?? ''); - const fullName = [firstName, lastName].filter(Boolean).join(' '); - const label = username - ? fullName ? `${username}: ${fullName}` : username - : fullName || item.id; - return { label, value: item.id }; - }), - }; - } catch { - return FAILED; - } +// --- Users (admin) ------------------------------------------------------------ + +export const userDropdown = makeSearchableDropdown({ + displayName: 'Owner', + description: + 'The back-office user who owns this record. Search by name, username or email.', + resourceUri: '/users', + fieldsParam: 'id,firstName,lastName,username', + searchExpr: (q) => `allText ~ "${q}"`, + labelFn: (item) => { + const firstName = String(item.attributes['firstName'] ?? ''); + const lastName = String(item.attributes['lastName'] ?? ''); + const username = String(item.attributes['username'] ?? ''); + const fullName = [firstName, lastName].filter(Boolean).join(' '); + return username + ? fullName + ? `${username}: ${fullName}` + : username + : fullName || item.id; }, }); // --- Websites ----------------------------------------------------------------- -export const websiteDropdown = Property.Dropdown({ - auth: oroAuth, +export const websiteDropdown = makeSearchableDropdown({ displayName: 'Website', - description: 'Search websites.', - required: false, - refreshers: [], - refreshOnSearch: true, - options: async ({ auth }, { searchValue }) => { - if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - 'fields[websites]': 'id,name' - }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; - } - const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/websites', queryParams: params }); - return { - options: items.map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + description: 'The website this record is associated with.', + resourceUri: '/websites', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), }); -// --- Invoice Internal Statuses ---------------------------------------------- -// Small enum — loaded once, no search needed. +// --- Invoice Internal Statuses ------------------------------------------------ -export const invoiceInternalStatusDropdown = Property.Dropdown({ - auth: oroAuth, +export const invoiceInternalStatusDropdown = makeEnumDropdown({ displayName: 'Internal Status', description: 'Invoice internal status (e.g. Draft, Open).', - required: false, - refreshers: [], - options: async ({ auth }) => { - if (!auth) return NOT_CONNECTED; - try { - const items = await fetchCollection({ - auth: auth as OroAuth, - resourceUri: '/invoiceinternalstatuses', - }); - return { - options: items.map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + resourceUri: '/invoiceinternalstatuses', + labelFn: attrLabel('name'), }); // --- Order Internal Statuses -------------------------------------------------- -// Small enum — loaded once, no search needed. -export const orderInternalStatusDropdown = Property.Dropdown({ - auth: oroAuth, +export const orderInternalStatusDropdown = makeEnumDropdown({ displayName: 'Internal Status', description: 'Order internal status (e.g. Open, Cancelled).', - required: false, - refreshers: [], - options: async ({ auth }) => { - if (!auth) return NOT_CONNECTED; - try { - const items = await fetchCollection({ - auth: auth as OroAuth, - resourceUri: '/orderinternalstatuses', - }); - return { - options: items.map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + resourceUri: '/orderinternalstatuses', + labelFn: attrLabel('name'), }); // --- Products ----------------------------------------------------------------- +// Kept custom: requires JSON:API `include` + relationship traversal to resolve +// the default-locale product name from the included `productnames` sideloads. export const productDropdown = Property.Dropdown({ auth: oroAuth, @@ -297,11 +287,14 @@ export const productDropdown = Property.Dropdown({ try { const params: Record = { 'fields[products]': 'id,sku,status,names', - 'include': 'names', + include: 'names', 'fields[productnames]': 'id,string,localization', }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}" and productStatus = "enabled"`; + const trimmed = searchValue?.trim() ?? ''; + if (trimmed.length > 0) { + params['filter[searchQuery]'] = `allText ~ "${sanitizeSearch( + trimmed + )}" and productStatus = "enabled"`; } else { params['filter[status]'] = 'enabled'; } @@ -315,32 +308,24 @@ export const productDropdown = Property.Dropdown({ const body = response.body as { data: OroJsonApiItem[]; - included?: { type: string; id: string; attributes: Record; relationships: Record }[]; + included?: { + type: string; + id: string; + attributes: Record; + relationships: Record; + }[]; }; - // Build a map: productId → default name (localization.data === null) - const nameMap: Record = {}; - for (const inc of body.included ?? []) { - if (inc.type === 'productnames') { - const localizationRel = inc.relationships['localization'] as { data: { type: string; id: string } | null } | undefined; - if (localizationRel?.data === null) { - const productRel = inc.relationships['product'] as { data: { type: string; id: string } | null } | undefined; - const productId = productRel?.data?.id; - if (productId) { - nameMap[productId] = String(inc.attributes['string'] ?? ''); - } - } - } - } + const nameMap = buildProductNameMap(body.included ?? []); return { options: (body.data ?? []).map((item) => { const sku = String(item.attributes['sku'] ?? ''); const name = nameMap[item.id] ?? ''; - const label = name - ? `${item.id}: ${sku} - ${name}` - : `${item.id}: ${sku}`; - return { label, value: item.id }; + return { + label: name ? `${item.id}: ${sku} - ${name}` : `${item.id}: ${sku}`, + value: item.id, + }; }), }; } catch { @@ -351,82 +336,32 @@ export const productDropdown = Property.Dropdown({ // --- Payment Terms ------------------------------------------------------------ -export const paymentTermDropdown = Property.Dropdown({ - auth: oroAuth, +export const paymentTermDropdown = makeSearchableDropdown({ displayName: 'Payment Term', description: 'Search payment terms.', - required: false, - refreshers: [], - refreshOnSearch: true, - options: async ({ auth }, { searchValue }) => { - if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - 'fields[paymentterms]': 'id,label' - }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `label ~ "${searchValue.trim().replace('"', '')}"`; - } - - const items = await fetchCollection({ - auth: auth as OroAuth, - resourceUri: '/paymentterms', - queryParams: params, - }); - return { - options: items.map((item) => ({ - label: String(item.attributes['label'] ?? item.id), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + resourceUri: '/paymentterms', + fieldsParam: 'id,label', + searchExpr: (q) => `label ~ "${q}"`, + labelFn: attrLabel('label'), }); // --- Warehouses --------------------------------------------------------------- -// Supports filter[searchText]. -export const warehouseDropdown = Property.Dropdown({ - auth: oroAuth, +export const warehouseDropdown = makeSearchableDropdown({ displayName: 'Warehouse', description: 'Search warehouses.', - required: false, - refreshers: [], - refreshOnSearch: true, - options: async ({ auth }, { searchValue }) => { - if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - 'fields[warehouses]': 'id,name' - }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; - } - const items = await fetchCollection({ - auth: auth as OroAuth, - resourceUri: '/warehouses', - queryParams: params, - }); - return { - options: items.map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + resourceUri: '/warehouses', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), }); // --- Countries ---------------------------------------------------------------- -// Static dictionary — load full list once per connection (cached by auth refresher). -// Uses filter[name] for client-side filtering via the UI's built-in search box. -// Page size 300 to capture all ~250 ISO countries in one request. +// Loads the full list (~250 ISO countries) once and filters client-side. -export const buildCountryDropdown = (required = false, displayName = 'Country') => +export const buildCountryDropdown = ( + { required = false, displayName = 'Country' }: { required?: boolean; displayName?: string } = {} +) => Property.Dropdown({ auth: oroAuth, displayName, @@ -436,32 +371,17 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') options: async ({ auth }, { searchValue }) => { if (!auth) return NOT_CONNECTED; try { - const params: Record = { - 'fields[countries]': 'id,name', - 'page[size]': '300', - }; - const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/countries', - queryParams: params, + queryParams: { 'fields[countries]': 'id,name', 'page[size]': '300' }, }); return { - options: items - .filter(function (item) { - if (!searchValue || searchValue?.length == 0) { - return true; - } - - const searchBase = String(item.attributes['name'] ?? item.id); - - return searchBase.toLowerCase().includes(searchValue.toLowerCase()); - }) - .map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, // ISO2 code e.g. "US" - })) - .sort((a, b) => a.label.localeCompare(b.label)), + options: clientSideFilterAndSort({ + items, + searchValue, + labelAttr: 'name', + }), }; } catch { return FAILED; @@ -470,14 +390,17 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') }); // --- Regions ------------------------------------------------------------------ -// Static dictionary per country — load all regions for the selected country once. -// Cached by auth + countryRefresher. Uses filter[country] to scope results. +// Loads all regions for the selected country once and filters client-side. -export const buildRegionDropdown = ( - countryRefresher: string, +export const buildRegionDropdown = ({ + countryRefresher, required = false, - displayName = 'Region / State' -) => + displayName = 'Region / State', +}: { + countryRefresher: string; + required?: boolean; + displayName?: string; +}) => Property.Dropdown({ auth: oroAuth, displayName, @@ -487,7 +410,6 @@ export const buildRegionDropdown = ( refreshers: [countryRefresher], options: async (context) => { const auth = context['auth'] as OroAuth | undefined; - const searchValue = context['searchValue'] as string | undefined; const countryId = context[countryRefresher as keyof typeof context] as | string | undefined; @@ -499,25 +421,17 @@ export const buildRegionDropdown = ( options: [], }; try { - const params: Record = {'filter[country]': countryId}; - - const items = await fetchCollection({ auth, resourceUri: '/regions', queryParams: params }); + const items = await fetchCollection({ + auth, + resourceUri: '/regions', + queryParams: { 'filter[country]': countryId }, + }); return { - options: items - .filter(function (item) { - if (!searchValue || searchValue?.length == 0) { - return true; - } - - const searchBase = String(item.attributes['name'] ?? item.id); - - return searchBase.toLowerCase().includes(searchValue.toLowerCase()); - }) - .map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, // ISO 3166-2 code e.g. "US-NY" - })) - .sort((a, b) => a.label.localeCompare(b.label)), + options: clientSideFilterAndSort({ + items, + searchValue: undefined, + labelAttr: 'name', + }), }; } catch { return FAILED; @@ -526,91 +440,150 @@ export const buildRegionDropdown = ( }); // --- Order Statuses (external) ------------------------------------------------ -// Small enum managed by an external system. Loaded once, no search needed. -export const orderStatusDropdown = Property.Dropdown({ - auth: oroAuth, +export const orderStatusDropdown = makeEnumDropdown({ displayName: 'Status', - description: 'Order status managed by an external system (only relevant when "Enable External Status Management" is on).', - required: false, - refreshers: [], - options: async ({ auth }) => { - if (!auth) return NOT_CONNECTED; - try { - const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/orderstatuses' }); - return { - options: items.map((item) => ({ - label: String(item.attributes['name'] ?? item.id), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + description: + 'Order status managed by an external system (only relevant when "Enable External Status Management" is on).', + resourceUri: '/orderstatuses', + labelFn: attrLabel('name'), }); -// --- Parent Order ------------------------------------------------------------- +// --- Orders ------------------------------------------------------------------- -export const orderDropdown = Property.Dropdown({ - auth: oroAuth, +export const orderDropdown = makeSearchableDropdown({ displayName: 'Parent Order', description: 'Search orders to use as the parent order.', - required: false, - refreshers: [], - refreshOnSearch: true, - options: async ({ auth }, { searchValue }) => { - if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - 'fields[orders]': 'id,identifier,poNumber', - }; - if (searchValue && searchValue.trim().length > 0) { - params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; - } - const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/orders', queryParams: params }); - return { - options: items.map((item) => ({ - label: String( - item.attributes['identifier'] ?? - item.attributes['poNumber'] ?? - item.id - ), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + resourceUri: '/orders', + fieldsParam: 'id,identifier,poNumber', + searchExpr: (q) => `allText ~ "${q}"`, + labelFn: attrLabel('identifier', 'poNumber'), }); // --- Product Units ------------------------------------------------------------ -// Small static list (each, set, kg, …) — load all once per connection. -export const productUnitDropdown = Property.Dropdown({ - auth: oroAuth, +export const productUnitDropdown = makeEnumDropdown({ displayName: 'Product Unit', description: 'Unit of measure for the product (e.g. each, set, kg).', + resourceUri: '/productunits', + labelFn: attrLabel('label', 'code'), + extraParams: { 'page[size]': '100' }, +}); + +// --- Customer Groups ---------------------------------------------------------- + +export const customerGroupDropdown = makeSearchableDropdown({ + displayName: 'Customer Group', + description: 'Search customer groups.', + resourceUri: '/customergroups', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +// --- Customer Tax Codes ------------------------------------------------------- + +export const customerTaxCodeDropdown = makeSearchableDropdown({ + displayName: 'Tax Code', + description: 'Search customer tax codes.', + resourceUri: '/customertaxcodes', + fieldsParam: 'id,code', + searchExpr: (q) => `code ~ "${q}"`, + labelFn: attrLabel('code'), +}); + +// --- Customer Ratings --------------------------------------------------------- + +export const customerRatingDropdown = makeEnumDropdown({ + displayName: 'Internal Rating', + description: 'Internal customer rating (e.g. "1 of 5", "5 of 5").', + resourceUri: '/customerratings', + labelFn: attrLabel('name'), + extraParams: { 'fields[customerratings]': 'id,name' }, +}); + +// --- Customer User Roles ------------------------------------------------------ + +export const customerUserRoleDropdown = makeSearchableDropdown({ + displayName: 'Roles', + description: 'Customer user roles. Search by label.', + resourceUri: '/customeruserroles', + fieldsParam: 'id,label,role', + searchExpr: (q) => `label ~ "${q}"`, + labelFn: attrLabel('label', 'role'), +}); + +// --- Additional Attributes / Relations (custom entity fields) ----------------- + +export const additionalAttributesProp = Property.Json({ + displayName: 'Additional Attributes', + description: + 'Optional JSON object with custom entity attributes. ' + + 'Keys are attribute names, values are their values. ' + + 'Example: {"myCustomField": "value", "priority": 5}. ' + + 'These are merged into the request body after the standard fields.', required: false, - refreshers: [], - options: async ({ auth }) => { - if (!auth) return NOT_CONNECTED; - try { - const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/productunits', queryParams: { - 'page[size]': '100', - } }); - return { - options: items.map((item) => ({ - // productunits use the string code as id (e.g. "each"), attributes may have label - label: String( - item.attributes['label'] ?? item.attributes['code'] ?? item.id - ), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + defaultValue: {}, }); + +export const additionalRelationsProp = Property.Json({ + displayName: 'Additional Relations', + description: + 'Optional JSON object with custom entity relationships. ' + + 'Each key is a relationship name and the value must follow JSON:API linkage format. ' + + 'Example: {"myRelation": {"data": {"type": "myentities", "id": "1"}}, ' + + '"myMultiRelation": {"data": [{"type": "otherentities", "id": "2"}]}}. ' + + 'These are merged into the request body after the standard relationships.', + required: false, + defaultValue: {}, +}); + +// --- Private helpers ---------------------------------------------------------- + +function buildProductNameMap( + included: { + type: string; + id: string; + attributes: Record; + relationships: Record; + }[] +): Record { + return included.reduce>((map, inc) => { + if (inc.type !== 'productnames') return map; + const localizationRel = inc.relationships['localization'] as + | { data: { type: string; id: string } | null } + | undefined; + if (localizationRel?.data !== null) return map; + const productRel = inc.relationships['product'] as + | { data: { type: string; id: string } | null } + | undefined; + const productId = productRel?.data?.id; + if (!productId) return map; + return { ...map, [productId]: String(inc.attributes['string'] ?? '') }; + }, {}); +} + +// --- Types -------------------------------------------------------------------- + +type LabelFn = (item: OroJsonApiItem) => string; + +type SearchableDropdownConfig = { + displayName: string; + description: string; + required?: boolean; + refreshers?: string[]; + resourceUri: string; + fieldsParam: string; + searchExpr: (sanitizedQuery: string) => string; + labelFn: LabelFn; + extraParams?: Record; +}; + +type EnumDropdownConfig = { + displayName: string; + description: string; + required?: boolean; + resourceUri: string; + labelFn: LabelFn; + extraParams?: Record; +}; diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index c7543c1e4b59..7cafbb188c92 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -18,7 +18,7 @@ export const oroWebhookTopicTrigger = createTrigger({ displayName: 'Topic', description: 'Only topics accessible by your connection are shown', required: true, - refreshers: ['auth'], // re-fetches when connection changes + refreshers: ['auth'], options: async ({ auth }) => { if (!auth) { return { disabled: true, placeholder: 'Connect your account first', options: [] }; @@ -27,14 +27,16 @@ export const oroWebhookTopicTrigger = createTrigger({ const response = await oroApiCall({ method: HttpMethod.GET, resourceUri: 'webhooktopics', - auth: auth as OroAuth + auth: auth as OroAuth, }); const body = response.body as OroJsonApiCollection; return { options: (body.data ?? []).map((item: OroJsonApiItem) => ({ label: String( - item.attributes['label'] ? item.attributes['label'] + ' (' + item.id + ')' : item.id + item.attributes['label'] + ? item.attributes['label'] + ' (' + item.id + ')' + : item.id ), value: item.id, })), @@ -58,7 +60,7 @@ export const oroWebhookTopicTrigger = createTrigger({ notificationUrl: context.webhookUrl, }, relationships: { - topic: { + topic: { data: { type: 'webhooktopics', id: context.propsValue.topic, @@ -75,16 +77,19 @@ export const oroWebhookTopicTrigger = createTrigger({ }, }); - await context.store.put(`webhookInfo`, { - webhookId: response.body.data.id, + const webhookId = (response.body as { data?: { id?: string } })?.data?.id; + if (!webhookId) { + throw new Error('OroCommerce webhook registration failed: no webhook ID returned. Check your connection and permissions.'); + } + + await context.store.put('webhookInfo', { + webhookId, topic: context.propsValue.topic, }); }, async onDisable(context) { - const webhookInfo = await context.store.get( - `webhookInfo` - ); + const webhookInfo = await context.store.get('webhookInfo'); if (webhookInfo !== null && webhookInfo !== undefined) { await oroApiCall({ From 3ada7b8b3da0f2c54e77efe51b1ed791a6c71ac2 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 27 May 2026 17:08:47 +0200 Subject: [PATCH 23/50] feat(orocommerce): fixed empty relations push --- .../src/lib/actions/create-customer-user.ts | 6 +++++- .../src/lib/actions/create-customer.ts | 6 +++++- .../src/lib/actions/create-invoice.ts | 6 +++++- .../src/lib/actions/create-order.ts | 6 +++++- .../src/lib/actions/update-customer-user.ts | 4 ++-- .../src/lib/actions/update-customer.ts | 4 ++-- .../orocommerce/src/lib/common/client.ts | 19 ++++++++++++++++++- .../src/lib/common/jsonapi-body-utils.ts | 4 ++-- 8 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts index 0851acf15199..3530e2819852 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -159,7 +159,11 @@ export const createCustomerUserAction = createAction({ }; const body: Record = { - data: { type: 'customerusers', attributes, relationships }, + data: { + type: 'customerusers', + attributes, + relationships + }, }; if (included.length > 0) body['included'] = included; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts index e49dc94cb4ee..e22e53b94509 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts @@ -120,7 +120,11 @@ export const createCustomerAction = createAction({ }; const body: Record = { - data: { type: 'customers', attributes, relationships }, + data: { + type: 'customers', + attributes, + relationships + }, }; if (included.length > 0) body['included'] = included; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index 1629359b062b..b27914e1b924 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -272,7 +272,11 @@ export const createInvoiceAction = createAction({ resourceUri: '/invoices', auth: context.auth, body: { - data: { type: 'invoices', attributes, relationships }, + data: { + type: 'invoices', + attributes, + relationships + }, included, }, }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index 14600677e876..14d75b3758b0 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -461,7 +461,11 @@ export const createOrderAction = createAction({ resourceUri: '/orders', auth: context.auth, body: { - data: { type: 'orders', attributes, relationships }, + data: { + type: 'orders', + attributes, + relationships + }, included, }, }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index 207e3a68b74d..b2d06931ac0a 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -133,12 +133,12 @@ export const updateCustomerUserAction = createAction({ resourceUri: `/customerusers/${p.customerUserId}`, auth: context.auth as OroAuth, body: { - data: jsonApiBodyUtils.omitEmptyObjects({ + data: { type: 'customerusers', id: p.customerUserId, attributes, relationships, - }), + }, }, }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts index 67c161c66cc4..bf4204d1133d 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts @@ -97,12 +97,12 @@ export const updateCustomerAction = createAction({ resourceUri: `/customers/${p.customerId}`, auth: context.auth as OroAuth, body: { - data: jsonApiBodyUtils.omitEmptyObjects({ + data: { type: 'customers', id: p.customerId, attributes, relationships, - }), + }, }, }); diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index 0d29d4441c1b..c823b87161d1 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -15,6 +15,7 @@ import { type OroJsonApiCollection, type FetchCollectionParams, } from './types'; +import { jsonApiBodyUtils } from './jsonapi-body-utils'; const tokenCache = new Map(); @@ -84,6 +85,7 @@ export async function oroApiCall({ }: OroApiCallParams): Promise> { try { const resource = resourceUri.replace(/^\/+/, ''); + const sanitizedBody = sanitizeJsonApiBody({ body }); return await httpClient.sendRequest({ method, @@ -97,7 +99,7 @@ export async function oroApiCall({ token: await getAccessToken({ auth }), }, queryParams, - body, + body: sanitizedBody, }); } catch (error: unknown) { throw new Error(formatError({ error })); @@ -119,3 +121,18 @@ export async function fetchCollection({ const body = response.body as OroJsonApiCollection | undefined; return body?.data ?? []; } + +function sanitizeJsonApiBody({ body }: { body: unknown }): unknown { + if (body === null || typeof body !== 'object' || Array.isArray(body)) { + return body; + } + const record = body as Record; + if (!('data' in record) || typeof record['data'] !== 'object' || record['data'] === null) { + return body; + } + return { + ...record, + data: jsonApiBodyUtils.omitEmptyObjects(record['data'] as Record), + }; +} + diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts index e408f845721e..71caf6fa0580 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts @@ -28,11 +28,11 @@ function buildRels( ): Record { return Object.fromEntries( Object.entries(map) - .filter(([, [, id]]) => id != null && (!Array.isArray(id) || id.length > 0)) + .filter(([, [, id]]) => id != null && id !== '' && (!Array.isArray(id) || id.length > 0)) .map(([key, [type, id, many]]) => [ key, Array.isArray(id) - ? { data: id.map((i) => ({ type, id: i })) } + ? { data: id.filter((i) => i !== '').map((i) => ({ type, id: i })) } : many ? { data: [{ type, id }] } : { data: { type, id } }, From 6691aa15b7be8bd217740c2865b3972afa22eeb3 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 28 May 2026 12:34:28 +0200 Subject: [PATCH 24/50] feat(orocommerce): added support of custom API headers for connection and actions --- .../orocommerce/src/lib/actions/api-call.ts | 16 ++++++++++++---- .../src/lib/actions/create-customer-user.ts | 3 +++ .../src/lib/actions/create-customer.ts | 3 +++ .../src/lib/actions/create-invoice.ts | 3 +++ .../src/lib/actions/create-order.ts | 3 +++ .../src/lib/actions/update-customer-user.ts | 3 +++ .../src/lib/actions/update-customer.ts | 3 +++ .../orocommerce/src/lib/common/auth.ts | 9 +++++++++ .../orocommerce/src/lib/common/client.ts | 18 +++++++++++++++++- .../orocommerce/src/lib/common/props.ts | 10 ++++++++++ 10 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts index 7dd5b29dc673..6bb6659c22c1 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -1,4 +1,5 @@ import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { tryCatchSync } from '@activepieces/shared'; import { oroAuth, getOroBaseUrl, getAccessToken } from '../common'; export const customApiCallAction = createCustomApiCallAction({ @@ -7,10 +8,17 @@ export const customApiCallAction = createCustomApiCallAction({ displayName: 'Custom API Call', description: 'Make a direct authenticated call to the OroCommerce JSON:API.', baseUrl: (auth) => auth ? getOroBaseUrl({ auth }) : '', - authMapping: async (auth) => ({ - Authorization: `Bearer ${await getAccessToken({ auth })}`, - 'Content-Type': 'application/vnd.api+json', - }), + authMapping: async (auth) => { + const { data } = tryCatchSync(() => JSON.parse(auth.props.headers ?? '{}') as unknown); + const connectionHeaders = + typeof data === 'object' && data !== null && !Array.isArray(data) + ? Object.fromEntries(Object.entries(data as Record).map(([k, v]) => [k, String(v)])) + : {}; + return { + Authorization: `Bearer ${await getAccessToken({ auth })}`, + ...connectionHeaders, + }; + }, props: { headers: { defaultValue: { diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts index 3530e2819852..31d918b40364 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -13,6 +13,7 @@ import { buildIncludedAddress, additionalAttributesProp, additionalRelationsProp, + additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; @@ -108,6 +109,7 @@ export const createCustomerUserAction = createAction({ }), additionalAttributes: additionalAttributesProp, additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, }, async run(context) { @@ -172,6 +174,7 @@ export const createCustomerUserAction = createAction({ resourceUri: '/customerusers', auth: context.auth as OroAuth, body, + headers: p.additionalHeaders as Record, }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts index e22e53b94509..177f16a6d671 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts @@ -15,6 +15,7 @@ import { buildIncludedAddress, additionalAttributesProp, additionalRelationsProp, + additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; @@ -75,6 +76,7 @@ export const createCustomerAction = createAction({ }), additionalAttributes: additionalAttributesProp, additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, }, async run(context) { @@ -133,6 +135,7 @@ export const createCustomerAction = createAction({ resourceUri: '/customers', auth: context.auth as OroAuth, body, + headers: p.additionalHeaders as Record, }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index b27914e1b924..cd8041f9f6e4 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -11,6 +11,7 @@ import { websiteDropdown, additionalAttributesProp, additionalRelationsProp, + additionalHeadersProp, } from '../common'; import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; @@ -182,6 +183,7 @@ export const createInvoiceAction = createAction({ }), additionalAttributes: additionalAttributesProp, additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, }, async run(context) { @@ -279,6 +281,7 @@ export const createInvoiceAction = createAction({ }, included, }, + headers: p.additionalHeaders as Record, }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index 14d75b3758b0..b48cd5cdf6bb 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -19,6 +19,7 @@ import { buildIncludedAddress, additionalAttributesProp, additionalRelationsProp, + additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; @@ -356,6 +357,7 @@ export const createOrderAction = createAction({ productSearch: productDropdown, additionalAttributes: additionalAttributesProp, additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, }, async run(context) { @@ -468,6 +470,7 @@ export const createOrderAction = createAction({ }, included, }, + headers: p.additionalHeaders as Record, }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index b2d06931ac0a..407c870cbf32 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -10,6 +10,7 @@ import { websiteDropdown, additionalAttributesProp, additionalRelationsProp, + additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; @@ -92,6 +93,7 @@ export const updateCustomerUserAction = createAction({ organization: organizationDropdown, additionalAttributes: additionalAttributesProp, additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, }, async run(context) { @@ -140,6 +142,7 @@ export const updateCustomerUserAction = createAction({ relationships, }, }, + headers: p.additionalHeaders as Record, }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts index bf4204d1133d..60f4fd3bf8b8 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts @@ -12,6 +12,7 @@ import { paymentTermDropdown, additionalAttributesProp, additionalRelationsProp, + additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; @@ -62,6 +63,7 @@ export const updateCustomerAction = createAction({ paymentTerm: paymentTermDropdown, additionalAttributes: additionalAttributesProp, additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, }, async run(context) { @@ -104,6 +106,7 @@ export const updateCustomerAction = createAction({ relationships, }, }, + headers: p.additionalHeaders as Record, }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 1b705f422f1e..e0139ac857f7 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -42,6 +42,15 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. 'The OAuth Client Secret from your OroCommerce OAuth application.', required: true, }), + headers: Property.LongText({ + displayName: 'Default HTTP Headers', + description: + 'JSON object of HTTP headers sent with every API request. ' + + 'Actions can override or extend these per-call. ' + + 'Example: {"Content-Type": "application/vnd.api+json", "X-Custom": "value"}', + required: false, + defaultValue: '{"Content-Type": "application/vnd.api+json"}', + }), }, validate: async ({ auth }): Promise<{ valid: true } | { valid: false; error: string }> => { diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index c823b87161d1..3ce0ad34d292 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -6,6 +6,7 @@ import { HttpError, AuthenticationType, } from '@activepieces/pieces-common'; +import { tryCatchSync } from '@activepieces/shared'; import { type OroAuth, @@ -86,12 +87,13 @@ export async function oroApiCall({ try { const resource = resourceUri.replace(/^\/+/, ''); const sanitizedBody = sanitizeJsonApiBody({ body }); + const connectionHeaders = parseConnectionHeaders({ raw: auth.props.headers }); return await httpClient.sendRequest({ method, url: `${getOroBaseUrl({ auth })}/${resource}`, headers: { - 'Content-Type': 'application/vnd.api+json', + ...connectionHeaders, ...extraHeaders, }, authentication: { @@ -136,3 +138,17 @@ function sanitizeJsonApiBody({ body }: { body: unknown }): unknown { }; } +function toStringRecord({ obj }: { obj: Record | undefined }): Record { + if (!obj) return {}; + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, String(v)]), + ); +} + +function parseConnectionHeaders({ raw }: { raw: string | null | undefined }): Record { + if (!raw) return {}; + const { data, error } = tryCatchSync(() => JSON.parse(raw) as unknown); + if (error || typeof data !== 'object' || data === null || Array.isArray(data)) return {}; + return toStringRecord({ obj: data as Record }); +} + diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index 70c78cd9b9c2..ed39971b6ad0 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -538,6 +538,16 @@ export const additionalRelationsProp = Property.Json({ defaultValue: {}, }); +export const additionalHeadersProp = Property.Object({ + displayName: 'Additional Headers', + description: + 'Optional HTTP headers to send with this request. ' + + 'These override or extend the default headers configured on the connection. ' + + 'Example: {"X-Include": "noHateoas;totalCount"}.', + required: false, + defaultValue: {}, +}); + // --- Private helpers ---------------------------------------------------------- function buildProductNameMap( From c54f9fb32f958d89a179f391789d2d7604244813 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 28 May 2026 14:18:15 +0200 Subject: [PATCH 25/50] feat(orocommerce): default header moved to connection for BC --- packages/pieces/community/orocommerce/src/lib/common/auth.ts | 3 +-- packages/pieces/community/orocommerce/src/lib/common/client.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index e0139ac857f7..2ddf304b4d6a 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -48,8 +48,7 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. 'JSON object of HTTP headers sent with every API request. ' + 'Actions can override or extend these per-call. ' + 'Example: {"Content-Type": "application/vnd.api+json", "X-Custom": "value"}', - required: false, - defaultValue: '{"Content-Type": "application/vnd.api+json"}', + required: false }), }, diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index 3ce0ad34d292..b3723d91a2b7 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -93,6 +93,7 @@ export async function oroApiCall({ method, url: `${getOroBaseUrl({ auth })}/${resource}`, headers: { + 'Content-Type': 'application/vnd.api+json', ...connectionHeaders, ...extraHeaders, }, @@ -151,4 +152,3 @@ function parseConnectionHeaders({ raw }: { raw: string | null | undefined }): Re if (error || typeof data !== 'object' || data === null || Array.isArray(data)) return {}; return toStringRecord({ obj: data as Record }); } - From 1b76955015517e722ca584d9d712a7749ea773da Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 22 Jun 2026 18:22:34 +0200 Subject: [PATCH 26/50] feat(orocommerce): added user resource actions --- .../pieces/community/orocommerce/src/index.ts | 4 + .../src/lib/actions/create-user.ts | 157 +++++++++++++++++ .../orocommerce/src/lib/actions/index.ts | 2 + .../src/lib/actions/update-user.ts | 160 ++++++++++++++++++ .../orocommerce/src/lib/common/props.ts | 63 +++++++ 5 files changed, 386 insertions(+) create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/create-user.ts create mode 100644 packages/pieces/community/orocommerce/src/lib/actions/update-user.ts diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index 4aee28e89afc..c410dc56f64e 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -9,6 +9,8 @@ import { updateCustomerAction, createCustomerUserAction, updateCustomerUserAction, + createUserAction, + updateUserAction, customApiCallAction, serializeJsonApiAction, unserializeJsonApiAction, @@ -29,6 +31,8 @@ export const orocommerce = createPiece({ updateCustomerAction, createCustomerUserAction, updateCustomerUserAction, + createUserAction, + updateUserAction, customApiCallAction, serializeJsonApiAction, unserializeJsonApiAction, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts new file mode 100644 index 000000000000..43ac155f2c78 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts @@ -0,0 +1,157 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + businessUnitRequiredDropdown, + businessUnitDropdown, + organizationDropdown, + organizationsDropdown, + userRoleDropdown, + userGroupDropdown, + userAuthStatusDropdown, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, +} from '../common'; +import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; + +export const createUserAction = createAction({ + auth: oroAuth, + name: 'create_user', + displayName: 'Create User', + description: 'Creates a new back-office user in OroCommerce.', + props: { + // -- Required attributes --------------------------------------------------- + username: Property.ShortText({ + displayName: 'Username', + description: 'Login name for the user. Must be unique.', + required: true, + }), + email: Property.ShortText({ + displayName: 'Email', + description: 'Email address of the user.', + required: true, + }), + password: Property.ShortText({ + displayName: 'Password', + description: 'Password for the new account. Must comply with the system security policy.', + required: true, + }), + firstName: Property.ShortText({ + displayName: 'First Name', + required: true, + }), + lastName: Property.ShortText({ + displayName: 'Last Name', + required: true, + }), + + // -- Optional attributes --------------------------------------------------- + namePrefix: Property.ShortText({ + displayName: 'Name Prefix', + description: 'Honorific (e.g. Mr., Ms., Dr.).', + required: false, + }), + middleName: Property.ShortText({ + displayName: 'Middle Name', + required: false, + }), + nameSuffix: Property.ShortText({ + displayName: 'Name Suffix', + description: 'Suffix (e.g. PhD, Jr.).', + required: false, + }), + title: Property.ShortText({ + displayName: 'Title', + description: 'Job title or position.', + required: false, + }), + phone: Property.ShortText({ + displayName: 'Phone', + required: false, + }), + birthday: Property.ShortText({ + displayName: 'Birthday', + description: 'Birth date in YYYY-MM-DD format.', + required: false, + }), + enabled: Property.Checkbox({ + displayName: 'Enabled', + description: 'When disabled the user cannot log in. Defaults to true.', + required: false, + defaultValue: true, + }), + + // -- Required relationships ------------------------------------------------ + owner: businessUnitRequiredDropdown, + + // -- Optional relationships ------------------------------------------------ + organization: organizationDropdown, + businessUnits: businessUnitDropdown, + userRoles: userRoleDropdown, + organizations: organizationsDropdown, + groups: userGroupDropdown, + authStatus: userAuthStatusDropdown, + + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, + }, + + async run(context) { + const p = context.propsValue; + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { + username: p.username, + email: p.email, + password: p.password, + firstName: p.firstName, + lastName: p.lastName, + enabled: p.enabled ?? true, + ...jsonApiBodyUtils.pickDefined({ + namePrefix: p.namePrefix, + middleName: p.middleName, + nameSuffix: p.nameSuffix, + title: p.title, + phone: p.phone, + birthday: p.birthday, + }), + ...extraAttrs, + }; + + const relationships = { + owner: { data: { type: 'businessunits', id: p.owner ?? '' } }, + ...jsonApiBodyUtils.buildRels({ + organization: ['organizations', p.organization], + businessUnits: ['businessunits', p.businessUnits, true], + userRoles: ['userroles', p.userRoles, true], + organizations: ['organizations', p.organizations, true], + groups: ['usergroups', p.groups, true], + auth_status: ['userauthstatuses', p.authStatus], + }), + ...extraRels, + }; + + const response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: '/users', + auth: context.auth as OroAuth, + body: { + data: { + type: 'users', + attributes, + relationships, + }, + }, + headers: p.additionalHeaders as Record, + }); + + return response.body; + }, +}); + diff --git a/packages/pieces/community/orocommerce/src/lib/actions/index.ts b/packages/pieces/community/orocommerce/src/lib/actions/index.ts index 1d436c1809ab..14f2eac00090 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/index.ts @@ -4,6 +4,8 @@ export { createCustomerAction } from './create-customer'; export { updateCustomerAction } from './update-customer'; export { createCustomerUserAction } from './create-customer-user'; export { updateCustomerUserAction } from './update-customer-user'; +export { createUserAction } from './create-user'; +export { updateUserAction } from './update-user'; export { customApiCallAction } from './api-call'; export { serializeJsonApiAction } from './serialize-jsonapi'; export { unserializeJsonApiAction } from './unserialize-jsonapi'; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts new file mode 100644 index 000000000000..a7fd45eeacaa --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -0,0 +1,160 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + businessUnitDropdown, + organizationDropdown, + organizationsDropdown, + userRoleDropdown, + userGroupDropdown, + userAuthStatusDropdown, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, +} from '../common'; +import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; + +export const updateUserAction = createAction({ + auth: oroAuth, + name: 'update_user', + displayName: 'Update User', + description: + 'Updates an existing back-office user in OroCommerce. Only provided fields are changed.', + props: { + // -- Target record --------------------------------------------------------- + userId: Property.ShortText({ + displayName: 'User ID', + description: 'The numeric ID of the user to update.', + required: true, + }), + + // -- Attributes ------------------------------------------------------------ + username: Property.ShortText({ + displayName: 'Username', + description: 'Updated login name for the user.', + required: false, + }), + email: Property.ShortText({ + displayName: 'Email', + description: 'Updated email address of the user.', + required: false, + }), + password: Property.ShortText({ + displayName: 'Password', + description: 'New password for the account. Must comply with the system security policy.', + required: false, + }), + firstName: Property.ShortText({ + displayName: 'First Name', + required: false, + }), + lastName: Property.ShortText({ + displayName: 'Last Name', + required: false, + }), + namePrefix: Property.ShortText({ + displayName: 'Name Prefix', + description: 'Honorific (e.g. Mr., Ms., Dr.).', + required: false, + }), + middleName: Property.ShortText({ + displayName: 'Middle Name', + required: false, + }), + nameSuffix: Property.ShortText({ + displayName: 'Name Suffix', + description: 'Suffix (e.g. PhD, Jr.).', + required: false, + }), + title: Property.ShortText({ + displayName: 'Title', + description: 'Job title or position.', + required: false, + }), + phone: Property.ShortText({ + displayName: 'Phone', + required: false, + }), + birthday: Property.ShortText({ + displayName: 'Birthday', + description: 'Birth date in YYYY-MM-DD format.', + required: false, + }), + enabled: Property.Checkbox({ + displayName: 'Enabled', + description: 'Enable or disable the user account.', + required: false, + }), + + // -- Relationships --------------------------------------------------------- + owner: businessUnitDropdown, + organization: organizationDropdown, + businessUnits: businessUnitDropdown, + userRoles: userRoleDropdown, + organizations: organizationsDropdown, + groups: userGroupDropdown, + authStatus: userAuthStatusDropdown, + + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, + }, + + async run(context) { + const p = context.propsValue; + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { + ...jsonApiBodyUtils.pickDefined({ + username: p.username, + email: p.email, + password: p.password, + firstName: p.firstName, + lastName: p.lastName, + enabled: p.enabled ?? undefined, + namePrefix: p.namePrefix, + middleName: p.middleName, + nameSuffix: p.nameSuffix, + title: p.title, + phone: p.phone, + birthday: p.birthday, + }), + ...extraAttrs, + }; + + const relationships = { + ...jsonApiBodyUtils.buildRels({ + owner: ['businessunits', p.owner], + organization: ['organizations', p.organization], + businessUnits: ['businessunits', p.businessUnits, true], + userRoles: ['userroles', p.userRoles, true], + organizations: ['organizations', p.organizations, true], + groups: ['usergroups', p.groups, true], + auth_status: ['userauthstatuses', p.authStatus], + }), + ...extraRels, + }; + + const response = await oroApiCall({ + method: HttpMethod.PATCH, + resourceUri: `/users/${p.userId}`, + auth: context.auth as OroAuth, + body: { + data: { + type: 'users', + id: p.userId, + attributes, + relationships, + }, + }, + headers: p.additionalHeaders as Record, + }); + + return response.body; + }, +}); + diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index ed39971b6ad0..db95f35623dd 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -481,6 +481,69 @@ export const customerGroupDropdown = makeSearchableDropdown({ labelFn: attrLabel('name'), }); +// --- Business Units ----------------------------------------------------------- + +export const businessUnitDropdown = makeSearchableDropdown({ + displayName: 'Business Unit', + description: 'Search business units.', + resourceUri: '/businessunits', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +export const businessUnitRequiredDropdown = makeSearchableDropdown({ + displayName: 'Owner (Business Unit)', + description: 'The business unit that owns this record.', + required: true, + resourceUri: '/businessunits', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +// --- User Roles (admin) ------------------------------------------------------- + +export const userRoleDropdown = makeSearchableDropdown({ + displayName: 'User Role', + description: 'Back-office user role. Search by label.', + resourceUri: '/userroles', + fieldsParam: 'id,label,role', + searchExpr: (q) => `label ~ "${q}"`, + labelFn: attrLabel('label', 'role'), +}); + +// --- User Groups -------------------------------------------------------------- + +export const userGroupDropdown = makeSearchableDropdown({ + displayName: 'User Group', + description: 'Back-office user group. Search by name.', + resourceUri: '/usergroups', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +// --- Organizations (multi) ---------------------------------------------------- + +export const organizationsDropdown = makeSearchableDropdown({ + displayName: 'Organizations', + description: 'Organizations the user has access to. Search by name.', + resourceUri: '/organizations', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +// --- User Auth Statuses ------------------------------------------------------- + +export const userAuthStatusDropdown = makeEnumDropdown({ + displayName: 'Auth Status', + description: 'Authentication status of the user (e.g. active, reset, locked).', + resourceUri: '/userauthstatuses', + labelFn: attrLabel('name', 'id'), +}); + // --- Customer Tax Codes ------------------------------------------------------- export const customerTaxCodeDropdown = makeSearchableDropdown({ From e383de2496dc60f26c53a59d1a1bb8898ff3d760 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Fri, 26 Jun 2026 17:46:41 +0200 Subject: [PATCH 27/50] feat(orocommerce): orocommerce piece ENV vars support --- .../orocommerce/src/lib/actions/api-call.ts | 4 +- .../orocommerce/src/lib/common/auth.ts | 8 ++++ .../orocommerce/src/lib/common/client.ts | 37 ++++++++++++++++--- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts index 6bb6659c22c1..14e8acbd6248 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -1,13 +1,13 @@ import { createCustomApiCallAction } from '@activepieces/pieces-common'; import { tryCatchSync } from '@activepieces/shared'; -import { oroAuth, getOroBaseUrl, getAccessToken } from '../common'; +import { oroAuth, getOroAdminApiBaseUrl, getAccessToken } from '../common'; export const customApiCallAction = createCustomApiCallAction({ auth: oroAuth, name: 'custom_api_call', displayName: 'Custom API Call', description: 'Make a direct authenticated call to the OroCommerce JSON:API.', - baseUrl: (auth) => auth ? getOroBaseUrl({ auth }) : '', + baseUrl: (auth) => auth ? getOroAdminApiBaseUrl({ auth }) : '', authMapping: async (auth) => { const { data } = tryCatchSync(() => JSON.parse(auth.props.headers ?? '{}') as unknown); const connectionHeaders = diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 2ddf304b4d6a..2b145bb06829 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -50,6 +50,14 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. 'Example: {"Content-Type": "application/vnd.api+json", "X-Custom": "value"}', required: false }), + isInternalInfrastructure: Property.Checkbox({ + displayName: 'Internal infrastructure', + description: + 'Setting this option allows to rewrite network-related connection options' + + ' with values provied in ENV variables.', + required: true, + defaultValue: false, + }) }, validate: async ({ auth }): Promise<{ valid: true } | { valid: false; error: string }> => { diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index b3723d91a2b7..533bc13256c2 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -21,7 +21,7 @@ import { jsonApiBodyUtils } from './jsonapi-body-utils'; const tokenCache = new Map(); function buildCacheKey({ auth }: { auth: OroAuth }): string { - return `${auth.props.serverUrl}::${auth.props.clientId}`; + return `${getOroServerUrl(auth)}::${auth.props.clientId}`; } function formatError({ error }: { error: unknown }): string { @@ -39,8 +39,33 @@ function formatError({ error }: { error: unknown }): string { return `OroCommerce API Error: ${String(error)}`; } -export function getOroBaseUrl({ auth }: { auth: OroAuth }): string { - const serverUrl = auth.props.serverUrl.replace(/\/*$/, ''); +function getOroServerUrl(auth: OroAuth): string { + const envUrl = isInternalInfrastructure({ auth }) + ? process.env['ORO_SERVER_URL']?.trim() + : undefined; + const url = envUrl || auth.props.serverUrl; + + return url.replace(/\/*$/, ''); +} + +function isInternalInfrastructure({ auth }: { auth: OroAuth }): boolean { + return auth.props.isInternalInfrastructure; +} + +function getInternalInfrastructureHeaders({ auth }: { auth: OroAuth }): Record { + if (!isInternalInfrastructure({ auth })) { + return {}; + } + const userAgent = process.env['ORO_SERVER_USER_AGENT']?.trim(); + if (!userAgent) { + return {}; + } + + return { 'User-Agent': userAgent }; +} + +export function getOroAdminApiBaseUrl({ auth }: { auth: OroAuth }): string { + const serverUrl = getOroServerUrl(auth); const adminPrefix = auth.props.adminPrefix.replace(/^\/+|\/+$/g, ''); return `${serverUrl}/${adminPrefix}/api`; } @@ -53,12 +78,13 @@ export async function getAccessToken({ auth }: { auth: OroAuth }): Promise({ method: HttpMethod.POST, url: `${baseUrl}/oauth2-token`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', + ...getInternalInfrastructureHeaders({ auth }), }, body: new URLSearchParams({ grant_type: 'client_credentials', @@ -91,10 +117,11 @@ export async function oroApiCall({ return await httpClient.sendRequest({ method, - url: `${getOroBaseUrl({ auth })}/${resource}`, + url: `${getOroAdminApiBaseUrl({ auth })}/${resource}`, headers: { 'Content-Type': 'application/vnd.api+json', ...connectionHeaders, + ...getInternalInfrastructureHeaders({ auth }), ...extraHeaders, }, authentication: { From 34b3ce825b67e975151485b23e99dca808ebe59e Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 20 Jul 2026 18:39:29 +0200 Subject: [PATCH 28/50] feat(orocommerce): upgrade orocommerce to new bundled structure --- .../community/orocommerce/.eslintrc.json | 18 ++++++++++++++-- .../pieces/community/orocommerce/package.json | 10 ++++++--- .../pieces/community/orocommerce/src/index.ts | 2 +- .../orocommerce/src/lib/actions/api-call.ts | 8 +++++-- .../orocommerce/src/lib/common/auth.ts | 2 +- .../orocommerce/src/lib/common/client.ts | 2 +- .../orocommerce/src/lib/common/index.ts | 2 ++ .../orocommerce/src/lib/common/try-catch.ts | 21 +++++++++++++++++++ 8 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/lib/common/try-catch.ts diff --git a/packages/pieces/community/orocommerce/.eslintrc.json b/packages/pieces/community/orocommerce/.eslintrc.json index 4a4e695c547e..01f1c80f85a4 100644 --- a/packages/pieces/community/orocommerce/.eslintrc.json +++ b/packages/pieces/community/orocommerce/.eslintrc.json @@ -20,7 +20,21 @@ "*.ts", "*.tsx" ], - "rules": {} + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "lodash", + "lodash/*", + "@activepieces/core-*", + "@activepieces/server*", + "@activepieces/engine", + "@activepieces/shared" + ] + } + ] + } }, { "files": [ @@ -30,4 +44,4 @@ "rules": {} } ] -} \ No newline at end of file +} diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index ace5d480c788..fd5d807fbeb7 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -6,11 +6,15 @@ "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", - "@activepieces/shared": "workspace:*", - "tslib": "2.6.2" + "@activepieces/core-piece-types": "workspace:*", + "@activepieces/core-utils": "workspace:*" }, "scripts": { "build": "tsc -p tsconfig.lib.json && cp package.json dist/", - "lint": "eslint 'src/**/*.ts'" + "lint": "eslint 'src/**/*.ts'", + "bundle": "node ../../../../dist/packages/cli/src/index.js pieces bundle" + }, + "devDependencies": { + "tslib": "2.6.2" } } diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index c410dc56f64e..bfd1ffcd685e 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -1,5 +1,5 @@ import { createPiece } from '@activepieces/pieces-framework'; -import { PieceCategory } from '@activepieces/shared'; +import { PieceCategory } from '@activepieces/pieces-framework'; import { oroAuth } from './lib/common'; import { oroWebhookTopicTrigger } from './lib/triggers/webhook-topic-trigger'; import { diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts index 14e8acbd6248..6c119e8f1342 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -1,6 +1,10 @@ import { createCustomApiCallAction } from '@activepieces/pieces-common'; -import { tryCatchSync } from '@activepieces/shared'; -import { oroAuth, getOroAdminApiBaseUrl, getAccessToken } from '../common'; +import { + getAccessToken, + getOroAdminApiBaseUrl, + oroAuth, + tryCatchSync, +} from '../common'; export const customApiCallAction = createCustomApiCallAction({ auth: oroAuth, diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 2b145bb06829..0b59f662a5c5 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -1,7 +1,7 @@ import { PieceAuth, Property } from '@activepieces/pieces-framework'; import { HttpMethod, HttpResponse, HttpMessageBody } from '@activepieces/pieces-common'; import { oroApiCall } from './client'; -import { AppConnectionType, tryCatch } from '@activepieces/shared'; +import { AppConnectionType, tryCatch } from '@activepieces/pieces-framework'; export const oroAuth = PieceAuth.CustomAuth({ description: ` diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index 533bc13256c2..ffb04c1df766 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -6,7 +6,6 @@ import { HttpError, AuthenticationType, } from '@activepieces/pieces-common'; -import { tryCatchSync } from '@activepieces/shared'; import { type OroAuth, @@ -17,6 +16,7 @@ import { type FetchCollectionParams, } from './types'; import { jsonApiBodyUtils } from './jsonapi-body-utils'; +import { tryCatchSync } from './try-catch'; const tokenCache = new Map(); diff --git a/packages/pieces/community/orocommerce/src/lib/common/index.ts b/packages/pieces/community/orocommerce/src/lib/common/index.ts index f8e52694be43..4a007e9de1c6 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -2,3 +2,5 @@ export * from './auth'; export * from './client'; export * from './props'; export * from './address-props'; +export * from './jsonapi-body-utils'; +export * from './try-catch'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/try-catch.ts b/packages/pieces/community/orocommerce/src/lib/common/try-catch.ts new file mode 100644 index 000000000000..36c24eb7426f --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/try-catch.ts @@ -0,0 +1,21 @@ +// copied from not-allowed to import activepieces/try-catch +type Success = { + data: T; + error: null; +}; + +type Failure = { + data: null; + error: E; +}; + +type Result = Success | Failure; + +export function tryCatchSync(fn: () => T): Result { + try { + const data = fn(); + return { data, error: null }; + } catch (error) { + return { data: null, error: error as E }; + } +} From 94d51913bfff92f9f5d64f9033493e48d9bfc094 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Fri, 7 Aug 2026 18:31:25 +0200 Subject: [PATCH 29/50] feat(orocommerce): piece refactoring, improved code quality --- .../pieces/community/orocommerce/README.md | 219 ++++- .../pieces/community/orocommerce/package.json | 6 +- .../plan-oroCommerceActions.prompt.md_ | 784 ++++++++++++++++++ .../orocommerce/src/lib/actions/api-call.ts | 21 +- .../src/lib/actions/create-customer-user.ts | 28 +- .../src/lib/actions/create-customer.ts | 24 +- .../src/lib/actions/create-invoice.ts | 4 +- .../src/lib/actions/create-order.ts | 234 +++--- .../src/lib/actions/create-user.ts | 29 +- .../src/lib/actions/serialize-jsonapi.ts | 101 ++- .../src/lib/actions/unserialize-jsonapi.ts | 10 +- .../src/lib/actions/update-customer-user.ts | 6 +- .../src/lib/actions/update-customer.ts | 2 +- .../src/lib/actions/update-user.ts | 17 +- .../src/lib/common/address-props.ts | 97 ++- .../orocommerce/src/lib/common/auth.ts | 5 +- .../orocommerce/src/lib/common/client.ts | 161 ++-- .../orocommerce/src/lib/common/index.ts | 3 +- .../body-utils.ts} | 0 .../src/lib/common/jsonapi/deserialize.ts | 47 +- .../src/lib/common/jsonapi/index.ts | 1 + .../src/lib/common/jsonapi/serialize.ts | 345 ++++---- .../src/lib/common/jsonapi/types.ts | 7 +- .../orocommerce/src/lib/common/props.ts | 302 +++++-- .../orocommerce/src/lib/common/try-catch.ts | 21 - .../orocommerce/src/lib/common/types.ts | 1 + .../orocommerce/src/lib/examples/invoice.ts | 117 --- .../orocommerce/src/lib/examples/order.ts | 637 -------------- .../src/lib/triggers/webhook-topic-trigger.ts | 26 +- .../test/jsonapi-roundtrip.test.ts | 243 ++++++ .../community/orocommerce/tsconfig.json | 3 + .../community/orocommerce/tsconfig.spec.json | 16 + .../community/orocommerce/vitest.config.ts | 18 + 33 files changed, 2149 insertions(+), 1386 deletions(-) create mode 100644 packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ rename packages/pieces/community/orocommerce/src/lib/common/{jsonapi-body-utils.ts => jsonapi/body-utils.ts} (100%) delete mode 100644 packages/pieces/community/orocommerce/src/lib/common/try-catch.ts delete mode 100644 packages/pieces/community/orocommerce/src/lib/examples/invoice.ts delete mode 100644 packages/pieces/community/orocommerce/src/lib/examples/order.ts create mode 100644 packages/pieces/community/orocommerce/test/jsonapi-roundtrip.test.ts create mode 100644 packages/pieces/community/orocommerce/tsconfig.spec.json create mode 100644 packages/pieces/community/orocommerce/vitest.config.ts diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 51b0d3a5f59e..2d5db4af285a 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -1,7 +1,218 @@ -# pieces-orocommerce +# OroCommerce piece -This library was generated with [Nx](https://nx.dev). +Actions and a webhook trigger for the OroCommerce back-office JSON:API. -## Building +This repo bans code comments, so the non-obvious rules of this piece live here. Read the sections +below before changing anything under `src/` — most of them exist because of a bug that is easy to +reintroduce. -Run `nx build pieces-orocommerce` to build the library. +## How it talks to Oro + +Two endpoints, both derived from the connection (`src/lib/common/auth.ts`): + +- `POST {serverUrl}/oauth2-token` — OAuth2 **client credentials**, form-encoded. +- `{serverUrl}/{adminPrefix}/api/...` — the back-office JSON:API, bearer token. + +Everything funnels through `oroApiCall()` in `src/lib/common/client.ts`, which builds the URL, sets +`Content-Type: application/vnd.api+json`, attaches the token, and normalises errors. The only +exception is `Custom API Call` (see *Headers*). + +`oroApiCall` wraps failures into a readable `Error` via `formatError`. Pass +`throwOriginalError: true` when the caller needs the `HttpError` to inspect a status code — the +trigger's `onDisable` does that to swallow 401/403/404 on an already-deleted webhook. + +Connection `validate` performs `GET regions/US-CA`. A connection whose client cannot read +`regions` is reported invalid even if the credentials are correct. + +| Where | What | +| --- | --- | +| `src/lib/common/client.ts` | token cache, request pipeline, error formatting, env overrides | +| `src/lib/common/props.ts` | every shared dropdown + the paging loader | +| `src/lib/common/jsonapi/` | flat ⇄ JSON:API conversion (`serialize` / `deserialize`) plus the `body-utils.ts` helpers the create/update actions assemble bodies with | +| `test/jsonapi-roundtrip.test.ts` | the round-trip contract described below | + +## The flat shape (read this first) + +`Unserialize JSON:API Response` flattens a JSON:API document into a plain object so the +Activepieces data selector can show `customer.name` instead of hunting through `included`. +`Serialize JSON:API Request` turns that flat object back into a valid request body. The two must +round-trip losslessly, and the flat shape is ambiguous — a relationship and an attribute can look +identical once nesting is gone. Hence markers. + +`deserialize` writes `_type` onto every value that came from a relationship, and uses two sentinels +for the cases where there is no related record to carry a marker: + +```json +{ "_type": null, "id": null } // NULL_RELATIONSHIP — a to-one relationship whose data is null +{ "_emptyToMany": true } // EMPTY_TO_MANY — a to-many relationship whose data is [] +``` + +Without them, `null` is indistinguishable from a null *attribute* and `[]` from an attribute that +is an empty array. `serialize` would then classify the field as an attribute, Oro would receive an +unknown attribute name, and the request would fail with a 400 — silently converting a relationship +into garbage on a fetch → modify → write flow. + +The sentinels are plain JSON objects on purpose. A flat object crosses step boundaries as JSON, so +anything not survivable by `JSON.parse(JSON.stringify(x))` — `undefined`, a `Symbol`, a class +instance — cannot be used as a marker. The `Object.freeze` on the constants only guards the module's +own copies; the values a flow sees are ordinary parsed objects. + +### Classification rules in `splitFlat` + +For each key (`_type` and `id` are consumed as the resource identity, never emitted as attributes): + +1. `{_type: null, ...}` → relationship, `data: null`. +2. `{_emptyToMany: true}` → relationship, `data: []`. +3. An array → relationship **only if every element** is linkage-like; otherwise the whole array is + an attribute. An array that mixes linkages with plain values **throws**, naming the property and + the index of the first offender — guessing either way would corrupt data, and the flat shape has + no way to express "some of these are relationships". +4. Otherwise linkage-like → to-one relationship. +5. Otherwise → attribute (including plain objects and arrays of plain values). + +Linkage-like means a `_type: string` marker, or a raw `{type, id}` pair so hand-written bodies work +too. A `_type`-marked value carrying more than `_type`/`id` is *hoisted*: it becomes a linkage in +`relationships` and a full resource in `included`. + +The `relationships` prop of the Serialize action wins over anything detected in `attributes`, and a +name listed there is never also emitted as an attribute. + +## Token cache + +`src/lib/common/client.ts` keeps a module-level `Map` of tokens, keyed on a SHA-256 of +**resolved server URL + client id + client secret**. + +The secret must stay in the key. It was omitted once, and two connections pointing at the same +server with the same client id but different secrets collided: the connection with the *wrong* +secret got a cache hit, borrowed the other connection's token, and `validate` cheerfully approved +it. Any field that can change which credentials a request actually uses belongs in the key. + +Also in there, and easy to break: + +- **Expiry skew** — the entry expires 30s before Oro says it does, so a token is never used in the + last moments of its life. +- **401 → invalidate → retry once.** `invalidateAccessToken` only evicts if the cached token is + still the one that just failed, so a parallel refresh is not thrown away. +- **In-flight coalescing.** Concurrent callers with the same key await one shared promise from + `inFlightTokenRequests` instead of each hammering `/oauth2-token`. + +## Headers + +Every action except `Custom API Call` goes through `oroApiCall`, where later wins: + +``` +Content-Type: application/vnd.api+json (built in) + → connection "Default HTTP Headers" + → internal-infrastructure User-Agent + → the step's Additional Headers +``` + +`Authorization` is passed separately as the request's `authentication` and cannot be overridden from +any of those. + +`Custom API Call` is built from the shared `createCustomApiCallAction` (`packages/pieces/common`), +which merges `{...stepHeaders, ...authMappingResult}` — the step's own headers land *first*, so +whatever `authMapping` returns would normally beat them. That is why `authMapping` in +`src/lib/actions/api-call.ts` re-applies `toHeaderRecord({ value: propsValue['headers'] })` after +the connection headers: it restores the same precedence as above. `propsValue` is the second +argument `createCustomApiCallAction` hands to `authMapping`; without using it the step's headers +would silently lose to the connection's. `Authorization` is appended last and always wins. + +## Dropdowns and paging + +All shared dropdowns are built from `loadDropdownOptions` in `src/lib/common/props.ts`, in two +paging modes: + +- **default** — one page (`page[size]=50` from `fetchCollection`). Used together with + `refreshOnSearch: true` and a `filter[searchQuery]` expression, so anything not on the first page + is still reachable by typing. +- **`exhaustive: true`** — walks pages of 100 until a short page arrives, capped at 20 pages + (2 000 records). + +The rule: **a prop with no server-side search must page exhaustively.** An option the user cannot +see does not exist to them, and for the multi-select "(replaces all existing …)" props an unseen +option is worse than missing — it means a role or business unit gets silently dropped from the +record on save. Enum-ish lists (statuses, units, regions) and every multi-select therefore use +`exhaustive`. Countries are the one hand-rolled exception: a single `page[size]=300` request covers +the whole ISO list, filtered client-side. + +Overflow is surfaced, not hidden: on hitting the 20-page cap the loader returns the options it has +plus a placeholder — `Showing the first N records only - more exist but are not listed`. Load +failures return a disabled dropdown with a "check the connection and its permissions" placeholder +rather than throwing, so one broken prop does not break the whole step. + +## Multi-selects deliberately have no `refreshOnSearch` + +Do not add it. `packages/web/src/components/custom/multi-select-piece-property.tsx` addresses +selections as **indices into the current options array**: it renders items with +`value: String(index)` and maps a change back with `options[Number(index)].value`. Selected indices +are resolved against `[...cachedOptions, ...options]`, while writes read `options` alone. + +With server-side search the options array is replaced on every keystroke, so indices held by the +form start pointing at different records — the user searches, and their existing selection quietly +becomes a different role. This is a limitation of shared web code, not a preference here; fixing it +means fixing the component to address selections by value. + +Single-value dropdowns are unaffected (`SearchableSelect` stores the value itself), which is why +they do use `refreshOnSearch: true`. + +## Update actions replace to-many relationships + +A JSON:API `PATCH` of a to-many relationship is a **full replace**, not a merge. So the roles, +groups, business-units and organizations props on the update actions overwrite the entire list — +which is why they are multi-selects labelled "(replaces all existing …)" and why their descriptions +tell the user to include everything the record should keep. Sending one role removes the others. + +## Creating related records in one request + +`create-order` and `create-customer-user` build addresses and line items as entries in `included` +with a made-up local id (`li_1`, `cu_addr_1`, `billing_address`) and reference that id from +`relationships`. That is Oro's extension for creating related resources alongside the primary one; +the temporary id is only a link target within the request and is replaced by the real id in the +response. `meta: { update: true }` is the *other* Oro convention — updating an existing related +record — and is not used here. + +`sanitizeJsonApiBody` in `client.ts` drops an empty `included: []` and any empty +`attributes: {}` / `relationships: {}` object from `data` before sending, so action code can build +those containers unconditionally without emitting empty ones on the wire. + +## Local development + +```bash +npx turbo run test --filter=@activepieces/piece-orocommerce # vitest (builds first) +npx turbo run lint --filter=@activepieces/piece-orocommerce +npx turbo run build --filter=@activepieces/piece-orocommerce # tsc -p tsconfig.lib.json, also the type-check +npm run lint-dev # repo-wide lint with auto-fix +``` + +There is no `typecheck` script in this package, so the root `typecheck` task is a no-op here — the +build is the type-check. + +**CI does not run these tests.** `.github/workflows/ci.yml` builds pieces touched by the diff, but +its test step is a hardcoded filter list (`@activepieces/engine`, `@activepieces/shared`, +`@activepieces/ai-providers`) that does not include this piece. `test/jsonapi-roundtrip.test.ts` +guards the serialize/deserialize contract above and is local-only today — run it yourself before +touching `src/lib/common/jsonapi/`. + +## Internal-infrastructure escape hatch + +The connection has an `isInternalInfrastructure` checkbox. When it is on, and only then, +`client.ts` reads two environment variables: + +- `ORO_SERVER_URL` — replaces the connection's Server URL. It applies to **both** the token endpoint + and the API base URL, and it is what the token cache key hashes, so flipping it does not reuse a + token minted for the old host. +- `ORO_SERVER_USER_AGENT` — adds a `User-Agent` header to the token request and to every API + request. + +Both are ignored when the checkbox is off or the variable is empty. The `adminPrefix`, client id and +client secret always come from the connection. + +## Gotchas + +- `loadDropdownOptions` derives the sparse-fieldset param as `fields[resourceUri.slice(1)]`, which + assumes `resourceUri` starts with `/`. Pass `'/customers'`, not `'customers'`, or you get + `fields[ustomers]` and a silently ignored fieldset. +- `Serialize JSON:API Request` accepts a single-resource document and unwraps it, but **rejects a + collection** (`data` is an array) with an explanatory error. Loop first. +- Piece i18n keys live in `src/i18n/translation.json` (identity-mapped English). diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index fd5d807fbeb7..1a4e03362845 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -11,10 +11,12 @@ }, "scripts": { "build": "tsc -p tsconfig.lib.json && cp package.json dist/", - "lint": "eslint 'src/**/*.ts'", - "bundle": "node ../../../../dist/packages/cli/src/index.js pieces bundle" + "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'", + "bundle": "node ../../../../dist/packages/cli/src/index.js pieces bundle", + "test": "vitest run" }, "devDependencies": { + "vitest": "3.2.6", "tslib": "2.6.2" } } diff --git a/packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ b/packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ new file mode 100644 index 000000000000..90f9a74a4e41 --- /dev/null +++ b/packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ @@ -0,0 +1,784 @@ +# OroCommerce Piece — Detailed Implementation Plan + +## Codebase Context + +### File Structure + +``` +packages/pieces/community/orocommerce/src/ +├── index.ts # Piece definition, registers all actions + triggers +├── lib/ +│ ├── actions/ +│ │ ├── index.ts # Re-exports all action modules +│ │ ├── api-call.ts # Custom API Call (utility) +│ │ ├── serialize-jsonapi.ts # Serialize JSON:API (utility) +│ │ ├── unserialize-jsonapi.ts # Unserialize JSON:API (utility) +│ │ ├── create-order.ts # ✅ Implemented +│ │ ├── create-invoice.ts # ✅ Implemented +│ │ ├── create-customer.ts # ✅ Implemented +│ │ ├── update-customer.ts # ✅ Implemented +│ │ ├── create-customer-user.ts # ✅ Implemented +│ │ └── update-customer-user.ts # ✅ Implemented +│ ├── common/ +│ │ ├── index.ts # Re-exports auth, client, props, address-props +│ │ ├── auth.ts # OAuth2 client_credentials auth +│ │ ├── client.ts # oroApiCall(), fetchCollection(), getOroBaseUrl(), getAccessToken() +│ │ ├── types.ts # OroAuth, OroApiCallParams, OroJsonApiItem, etc. +│ │ ├── props.ts # All dropdown props + additionalAttributes/Relations props +│ │ ├── address-props.ts # buildIncludedAddress(), baseAddressArrayItemProps, addressTypeProps +│ │ ├── jsonapi-body-utils.ts # pickDefined(), buildRels(), parseAdditionalAttributes/Relations() +│ │ └── jsonapi/ # Serialize/unserialize JSON:API helpers +│ ├── examples/ # Sample response data +│ └── triggers/ +│ └── webhook-topic-trigger.ts # Webhook trigger +``` + +### Key Architectural Patterns + +**Every action follows this exact pattern:** + +1. Import `createAction`, `Property` from `@activepieces/pieces-framework` +2. Import `HttpMethod` from `@activepieces/pieces-common` +3. Import `oroAuth`, `oroApiCall`, relevant dropdown props from `../common` +4. Import `OroAuth` from `../common/types` +5. Import `jsonApiBodyUtils` from `../common/jsonapi-body-utils` +6. Define `props` with required fields first, then optional attributes, then relationships, then `additionalAttributes` + `additionalRelations` last +7. In `run()`: extract `p = context.propsValue`, build `attributes` with `jsonApiBodyUtils.pickDefined()`, build `relationships` with `jsonApiBodyUtils.buildRels()`, merge extras, call `oroApiCall()`, return `response.body` + +**Create vs Update pattern:** + +- **Create**: `HttpMethod.POST`, resourceUri = `/entityname`, body includes `data.type` (no `id`) +- **Update**: `HttpMethod.PATCH`, resourceUri = `/entityname/${p.entityId}`, body includes `data.type` AND `data.id`, all fields optional + +**Dropdown prop types (in `props.ts`):** + +- `makeSearchableDropdown({...})` — for entities with server-side search (uses `filter[searchQuery]`) +- `makeEnumDropdown({...})` — for small enum/status lists (loads all, no search) +- Both use `oroAuth`, return `NOT_CONNECTED` / `FAILED` sentinels on error +- `attrLabel('attr1', 'attr2')` — creates label function that picks first non-null attribute + +**`jsonApiBodyUtils` helpers:** + +- `pickDefined({key: value})` — strips null/undefined entries +- `buildRels({relName: ['type', id]})` — builds JSON:API relationship linkage, skips null/empty ids +- `parseAdditionalAttributes(raw)` / `parseAdditionalRelations(raw)` — parse user JSON overrides +- `omitEmptyObjects(obj)` — strips keys with `{}` values + +**`buildIncludedAddress()` (from `address-props.ts`):** + +- Builds a JSON:API `included` resource for inline address creation +- Used for entities with address sub-resources (customers, customer users, orders) + +### Registration Checklist (for every new action) + +1. Create action file in `src/lib/actions/` +2. Add export to `src/lib/actions/index.ts` +3. Add import + registration in `src/index.ts` (both in the import block and the `actions: [...]` array) + +--- + +## Phase 1: Core Commerce (4 actions) + +### 1.1 Create Product — `create-product.ts` + +**API endpoint:** `POST /admin/api/products` +**JSON:API type:** `products` + +**Props:** + +| Prop Name | Type | Required | Description | +|------------------------|--------------------------------------------|----------|------------------------------------------------------------------------| +| `sku` | `ShortText` | ✅ | Product SKU code | +| `status` | `StaticDropdown` | ✅ | `enabled` / `disabled` (default: `disabled`) | +| `productType` | `StaticDropdown` | No | `simple` / `configurable` / `kit` (default: `simple`) | +| `featured` | `Checkbox` | No | Mark as featured | +| `newArrival` | `Checkbox` | No | Mark as new arrival | +| `availabilityDate` | `ShortText` | No | YYYY-MM-DDTHH:mm:ssZ | +| `categorySortOrder` | `Number` | No | Sort order in catalog category | +| `productName` | `ShortText` | ✅ | Default-locale product name (creates `productnames` included resource) | +| `productDescription` | `LongText` | No | Default-locale description (creates `productdescriptions` included) | +| `shortDescription` | `LongText` | No | Default-locale short description | +| `attributeFamily` | **New dropdown** `attributeFamilyDropdown` | ✅ | Required relationship | +| `productUnit` | Existing `productUnitDropdown` | ✅ | Primary unit (creates `productunitprecisions` included) | +| `unitPrecision` | `Number` | No | Decimal precision for the primary unit (default: 0) | +| `inventoryStatus` | **New dropdown** `inventoryStatusDropdown` | No | In Stock / Out of Stock | +| `category` | **New dropdown** `categoryDropdown` | No | Master catalog category | +| `brand` | **New dropdown** `brandDropdown` | No | Product brand | +| `taxCode` | **New dropdown** `productTaxCodeDropdown` | No | Product tax code | +| `organization` | Existing `organizationDropdown` | No | | +| `owner` | Existing `userDropdown` | No | | +| `additionalAttributes` | Existing | No | | +| `additionalRelations` | Existing | No | | + +**Implementation notes:** + +- Product creation requires `included` resources for localized fields (`productnames`, `productdescriptions`, `productshortdescriptions`). Each included resource must have: + ```json + { + "type": "productnames", + "id": "product_name_default", + "attributes": { "string": "Product Name", "fallback": null }, + "relationships": { "localization": { "data": null } } + } + ``` + Setting `localization.data` to `null` means "default locale." +- `primaryUnitPrecision` requires an included `productunitprecisions` resource: + ```json + { + "type": "productunitprecisions", + "id": "primary_unit", + "attributes": { "precision": 0, "conversionRate": 1, "sell": true }, + "relationships": { "unit": { "data": { "type": "productunits", "id": "each" } } } + } + ``` +- The relationship linkage for names/descriptions/shortDescriptions is an array of `{ type, id }` refs pointing to the included resources. + +**New dropdowns needed in `props.ts`:** + +``` +attributeFamilyDropdown → makeSearchableDropdown, /attributefamilies, fieldsParam: 'id,code', searchExpr: code ~ "q", labelFn: attrLabel('code'), required: true +productTaxCodeDropdown → makeSearchableDropdown, /producttaxcodes, fieldsParam: 'id,code', searchExpr: code ~ "q", labelFn: attrLabel('code') +inventoryStatusDropdown → makeEnumDropdown, /productinventorystatuses, labelFn: attrLabel('name') +categoryDropdown → makeSearchableDropdown, /mastercatalogcategories, fieldsParam: 'id,title', searchExpr: title ~ "q", labelFn: attrLabel('title') +brandDropdown → makeSearchableDropdown, /brands, fieldsParam: 'id', searchExpr: allText ~ "q", labelFn: item => item.id +``` + +### 1.2 Update Product — `update-product.ts` + +**API endpoint:** `PATCH /admin/api/products/{id}` +**JSON:API type:** `products` + +**Props:** Same as Create Product but: + +- Add `productId` (`ShortText`, required) — the ID to update +- All other fields become optional (including `sku`, `attributeFamily`) +- `productName`, `productDescription`, `shortDescription` — if provided, send as included resources same as create + +### 1.3 Update Order — `update-order.ts` + +**API endpoint:** `PATCH /admin/api/orders/{id}` +**JSON:API type:** `orders` + +**Props:** + +| Prop Name | Type | Required | +|--------------------------------|-------------------------------|----------| +| `orderId` | `ShortText` | ✅ | +| `currency` | `ShortText` | No | +| `poNumber` | `ShortText` | No | +| `customerNotes` | `LongText` | No | +| `shipUntil` | `ShortText` | No | +| `overriddenShippingCostAmount` | `Number` | No | +| `shippingMethod` | `ShortText` | No | +| `shippingMethodType` | `ShortText` | No | +| `disablePromotions` | `Checkbox` | No | +| `customer` | `customerDropdown` (optional) | No | +| `customerUser` | `customerUserDropdown(false)` | No | +| `organization` | `organizationDropdown` | No | +| `owner` | `userDropdown` | No | +| `website` | `websiteDropdown` | No | +| `internalStatus` | `orderInternalStatusDropdown` | No | +| `paymentTerm` | `paymentTermDropdown` | No | +| `warehouse` | `warehouseDropdown` | No | +| `status` | `orderStatusDropdown` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**Pattern:** Follow `update-customer.ts` exactly — `PATCH`, all fields optional, use `pickDefined` + `buildRels`. + +### 1.4 Update Invoice — `update-invoice.ts` + +**API endpoint:** `PATCH /admin/api/invoices/{id}` +**JSON:API type:** `invoices` + +**Props:** + +| Prop Name | Type | Required | +|------------------------|---------------------------------|----------| +| `invoiceId` | `ShortText` | ✅ | +| `invoiceDate` | `ShortText` | No | +| `currency` | `ShortText` | No | +| `customerName` | `ShortText` | No | +| `totalAmount` | `Number` | No | +| `invoiceNumber` | `ShortText` | No | +| `title` | `ShortText` | No | +| `description` | `LongText` | No | +| `memo` | `ShortText` | No | +| `billTo` | `LongText` | No | +| `shipTo` | `LongText` | No | +| `shippingMethod` | `ShortText` | No | +| `sellerInfo` | `LongText` | No | +| `externalPaymentUrl` | `ShortText` | No | +| `customer` | `customerDropdown` | No | +| `customerUser` | `customerUserDropdown(false)` | No | +| `organization` | `organizationDropdown` | No | +| `owner` | `userDropdown` | No | +| `website` | `websiteDropdown` | No | +| `internalStatus` | `invoiceInternalStatusDropdown` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +--- + +## Phase 2: B2B Workflow (2 actions) + +### 2.1 Create Quote — `create-quote.ts` + +**API endpoint:** `POST /admin/api/quotes` +**JSON:API type:** `quotes` + +**Attributes from spec:** + +- `currency`, `poNumber`, `validUntil`, `shipUntil`, `projectName` + +**Relationships from spec:** + +- `customer` (customers), `customerUser` (customerusers), `organization` (organizations), `owner` (users), `website` (websites) +- `internalStatus` (quoteinternalstatuses), `customerStatus` (quotecustomerstatuses) +- `opportunity` (opportunities), `paymentTerm` (paymentterms) +- `quoteProducts` (quoteproducts) — line items via included resources +- `shippingAddress` (quoteshippingaddresses) — inline included address + +**Props:** + +| Prop Name | Type | Required | +|------------------------|----------------------------------------|--------------------| +| `currency` | `ShortText` | ✅ (default: 'USD') | +| `customer` | `customerRequiredDropdown` | ✅ | +| `poNumber` | `ShortText` | No | +| `validUntil` | `ShortText` | No | +| `shipUntil` | `ShortText` | No | +| `projectName` | `ShortText` | No | +| `customerUser` | `customerUserDropdown(false)` | No | +| `organization` | `organizationDropdown` | No | +| `owner` | `userDropdown` | No | +| `website` | `websiteDropdown` | No | +| `internalStatus` | **New** `quoteInternalStatusDropdown` | No | +| `customerStatus` | **New** `quoteCustomerStatusDropdown` | No | +| `opportunity` | **New** `opportunityDropdown` | No | +| `paymentTerm` | `paymentTermDropdown` | No | +| `shippingAddress*` | Address fields (same pattern as order) | No | +| `lineItems` | `DynamicProperties` → `Property.Array` | ✅ | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**Line item sub-props** (inside `Property.Array`): + +- `productSku` (ShortText, required), `productId` (ShortText), `freeFormProduct` (ShortText) +- `comment` (LongText) +- Quote products have `quoteProductOffers` which specify price tiers — each offer has: `quantity` (Number, required), `value` (Number, required), `currency` (ShortText), `productUnit` (StaticDropdown from fetched units), `allowIncrements` (Checkbox) + +**Included resources structure:** + +```json +{ + "type": "quoteproducts", + "id": "qp_1", + "attributes": { + "comment": "...", + "freeFormProduct": "..." + }, + "relationships": { + "product": { + "data": { + "type": "products", + "id": "42" + } + }, + "quoteProductOffers": { + "data": [ + { + "type": "quoteproductoffers", + "id": "qpo_1_1" + } + ] + } + } +} +``` + +And each offer: + +```json +{ + "type": "quoteproductoffers", + "id": "qpo_1_1", + "attributes": { + "quantity": 10, + "value": "99.99", + "currency": "USD", + "allowIncrements": false + }, + "relationships": { + "productUnit": { + "data": { + "type": "productunits", + "id": "each" + } + } + } +} +``` + +**New dropdowns needed:** + +``` +quoteInternalStatusDropdown → makeEnumDropdown, /quoteinternalstatuses, labelFn: attrLabel('name') +quoteCustomerStatusDropdown → makeEnumDropdown, /quotecustomerstatuses, labelFn: attrLabel('name') +opportunityDropdown → makeSearchableDropdown, /opportunities, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') +``` + +### 2.2 Create RFQ — `create-rfq.ts` + +**API endpoint:** `POST /admin/api/rfqs` +**JSON:API type:** `rfqs` + +**Attributes from spec:** + +- `firstName`, `lastName`, `email`, `company`, `phone`, `role` +- `poNumber`, `note`, `shipUntil`, `projectName` + +**Relationships:** + +- `customer` (customers), `customerUser` (customerusers), `organization` (organizations), `owner` (users), `website` (websites) +- `internal_status` (rfqinternalstatuses), `customer_status` (rfqcustomerstatuses) +- `requestProducts` (rfqproducts) — line items via included + +**Props:** + +| Prop Name | Type | Required | +|------------------------|-------------------------------|----------| +| `firstName` | `ShortText` | ✅ | +| `lastName` | `ShortText` | ✅ | +| `email` | `ShortText` | ✅ | +| `company` | `ShortText` | No | +| `phone` | `ShortText` | No | +| `role` | `ShortText` | No | +| `poNumber` | `ShortText` | No | +| `note` | `LongText` | No | +| `shipUntil` | `ShortText` | No | +| `projectName` | `ShortText` | No | +| `customer` | `customerDropdown` | No | +| `customerUser` | `customerUserDropdown(false)` | No | +| `organization` | `organizationDropdown` | No | +| `owner` | `userDropdown` | No | +| `website` | `websiteDropdown` | No | +| `lineItems` | `Property.Array` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**Line item sub-props:** + +- `productSku` (ShortText, required), `productId` (ShortText), `comment` (LongText) +- Each RFQ product has `rfqProductItems` — each item has: `quantity` (Number, required), `value` (Number), `currency` (ShortText), `productUnit` (ShortText — use code like "each") + +**Included resources:** + +```json +{ + "type": "rfqproducts", + "id": "rp_1", + "relationships": { + "product": { + "data": { + "type": "products", + "id": "42" + } + }, + "requestProductItems": { + "data": [ + { + "type": "rfqproductitems", + "id": "rpi_1_1" + } + ] + } + } +} +``` + +--- + +## Phase 3: Pricing & Fulfillment (5 actions) + +### 3.1 Create Price List — `create-price-list.ts` + +**API endpoint:** `POST /admin/api/pricelists` +**JSON:API type:** `pricelists` + +**Props:** + +| Prop Name | Type | Required | +|-------------------------|------------------------|--------------------| +| `name` | `ShortText` | ✅ | +| `active` | `Checkbox` | No (default: true) | +| `productAssignmentRule` | `LongText` | No | +| `organization` | `organizationDropdown` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +Simple action — no included resources needed. + +### 3.2 Update Price List — `update-price-list.ts` + +**API endpoint:** `PATCH /admin/api/pricelists/{id}` +Same props as create but all optional + `priceListId` required. + +### 3.3 Create Product Price — `create-product-price.ts` + +**API endpoint:** `POST /admin/api/productprices` +**JSON:API type:** `productprices` + +**Props:** + +| Prop Name | Type | Required | +|------------------------|-----------------------------|--------------------| +| `value` | `Number` | ✅ | +| `currency` | `ShortText` | ✅ (default: 'USD') | +| `quantity` | `Number` | ✅ | +| `priceList` | **New** `priceListDropdown` | ✅ | +| `product` | `productDropdown` | ✅ | +| `unit` | `productUnitDropdown` | ✅ | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +Simple action — three required relationships, three required attributes. + +**New dropdown:** + +``` +priceListDropdown → makeSearchableDropdown, /pricelists, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') +``` + +### 3.4 Update Product Price — `update-product-price.ts` + +**API endpoint:** `PATCH /admin/api/productprices/{id}` +Same props but all optional + `productPriceId` required. + +### 3.5 Create Shipping Tracking — `create-shipping-tracking.ts` + +**API endpoint:** `POST /admin/api/ordershippingtrackings` +**JSON:API type:** `ordershippingtrackings` + +**Props:** + +| Prop Name | Type | Required | +|------------------------|--------------------------------------------------|----------| +| `method` | `ShortText` | ✅ | +| `number` | `ShortText` | ✅ | +| `order` | `orderDropdown` (modify to use required variant) | ✅ | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +Very simple action — 2 attributes, 1 relationship (note: the relationship key is `orders` plural per the spec, and the linkage uses `{ type: 'orders', id }` with a many-to-one wrapper `{ data: [{ type: 'orders', id }] }`). + +--- + +## Phase 4: CRM Entities (6 actions) + +### 4.1 Create Contact — `create-contact.ts` + +**API endpoint:** `POST /admin/api/contacts` +**JSON:API type:** `contacts` + +**Props:** + +| Prop Name | Type | Required | +|------------------------|---------------------------------------------------|----------------------------------------------------------------------------| +| `firstName` | `ShortText` | No (conditionally required: either firstName, lastName, emails, or phones) | +| `lastName` | `ShortText` | No | +| `primaryEmail` | `ShortText` | No | +| `primaryPhone` | `ShortText` | No | +| `emails` | `Property.Json` | No — array of `[{"email": "..."}]` | +| `phones` | `Property.Json` | No — array of `[{"phone": "..."}]` | +| `jobTitle` | `ShortText` | No | +| `description` | `LongText` | No | +| `namePrefix` | `ShortText` | No | +| `middleName` | `ShortText` | No | +| `nameSuffix` | `ShortText` | No | +| `gender` | `StaticDropdown` | No — `male` / `female` | +| `birthday` | `ShortText` | No — YYYY-MM-DD | +| `fax` | `ShortText` | No | +| `skype` | `ShortText` | No | +| `twitter` | `ShortText` | No | +| `facebook` | `ShortText` | No | +| `linkedIn` | `ShortText` | No | +| `externalId` | `ShortText` | No | +| `source` | **New** `contactSourceDropdown` | No | +| `method` | **New** `contactMethodDropdown` | No | +| `owner` | `userDropdown` | No | +| `organization` | `organizationDropdown` | No | +| `addresses` | `Property.Array` with `baseAddressArrayItemProps` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**Contact-specific notes:** + +- `emails` and `phones` are structured arrays in the JSON:API attributes (not relationships). Format: `[{"email": "addr@example.com"}]` and `[{"phone": "555-1234"}]` +- `source` is a relationship to `contactsources` (enumerable) +- `method` is a relationship to `contactmethods` (enumerable) +- Addresses use `contactaddresses` type in included resources + +**New dropdowns:** + +``` +contactSourceDropdown → makeEnumDropdown, /contactsources, labelFn: attrLabel('label') +contactMethodDropdown → makeEnumDropdown, /contactmethods, labelFn: attrLabel('label') +contactDropdown → makeSearchableDropdown, /contacts, fieldsParam: 'id,firstName,lastName,primaryEmail', searchExpr: allText ~ "q", labelFn: composite name+email +``` + +### 4.2 Update Contact — `update-contact.ts` + +Same props as Create Contact but all optional + `contactId` (ShortText, required). No address creation on update (addresses are managed separately). Follow Update Customer pattern. + +### 4.3 Create Lead — `create-lead.ts` + +**API endpoint:** `POST /admin/api/leads` +**JSON:API type:** `leads` + +**Props:** + +| Prop Name | Type | Required | +|------------------------|---------------------------------------------------|---------------------------------------------------------------| +| `name` | `ShortText` | ✅ | +| `firstName` | `ShortText` | No | +| `lastName` | `ShortText` | No | +| `companyName` | `ShortText` | No | +| `primaryEmail` | `ShortText` | No | +| `primaryPhone` | `ShortText` | No | +| `emails` | `Property.Json` | No | +| `phones` | `Property.Json` | No | +| `jobTitle` | `ShortText` | No | +| `website` | `ShortText` | No (note: this is a text attribute, not the website dropdown) | +| `namePrefix` | `ShortText` | No | +| `middleName` | `ShortText` | No | +| `nameSuffix` | `ShortText` | No | +| `numberOfEmployees` | `Number` | No | +| `industry` | `ShortText` | No | +| `twitter` | `ShortText` | No | +| `linkedIn` | `ShortText` | No | +| `notes` | `LongText` | No | +| `externalId` | `ShortText` | No | +| `status` | **New** `leadStatusDropdown` | No | +| `source` | **New** `leadSourceDropdown` | No | +| `contact` | **New** `contactDropdown` | No | +| `customer` | `customerDropdown` | No | +| `account` | **New** `accountDropdown` | No | +| `owner` | `userDropdown` | No | +| `organization` | `organizationDropdown` | No | +| `addresses` | `Property.Array` with `baseAddressArrayItemProps` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**Lead-specific notes:** + +- Lead addresses use type `leadaddresses` in included resources +- `emails`/`phones` same structure as contacts + +**New dropdowns:** + +``` +leadStatusDropdown → makeEnumDropdown, /leadstatuses, labelFn: attrLabel('name') +leadSourceDropdown → makeEnumDropdown, /leadsources, labelFn: attrLabel('name') +accountDropdown → makeSearchableDropdown, /accounts, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') +leadDropdown → makeSearchableDropdown, /leads, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') +``` + +### 4.4 Update Lead — `update-lead.ts` + +Same as Create Lead but all optional + `leadId` required. No address creation. Follow Update Customer pattern. + +### 4.5 Create Opportunity — `create-opportunity.ts` + +**API endpoint:** `POST /admin/api/opportunities` +**JSON:API type:** `opportunities` + +**Props:** + +| Prop Name | Type | Required | +|------------------------|------------------------------------------|-----------------| +| `name` | `ShortText` | ✅ | +| `budgetAmountValue` | `Number` | No | +| `budgetAmountCurrency` | `ShortText` | No | +| `closeDate` | `ShortText` | No — YYYY-MM-DD | +| `closeRevenueValue` | `Number` | No | +| `closeRevenueCurrency` | `ShortText` | No | +| `probability` | `Number` | No — 0.0 to 1.0 | +| `customerNeed` | `LongText` | No | +| `proposedSolution` | `LongText` | No | +| `notes` | `LongText` | No | +| `externalId` | `ShortText` | No | +| `status` | **New** `opportunityStatusDropdown` | No | +| `closeReason` | **New** `opportunityCloseReasonDropdown` | No | +| `contact` | `contactDropdown` | No | +| `customer` | `customerDropdown` | No | +| `account` | `accountDropdown` | No | +| `lead` | `leadDropdown` | No | +| `owner` | `userDropdown` | No | +| `organization` | `organizationDropdown` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**New dropdowns:** + +``` +opportunityStatusDropdown → makeEnumDropdown, /opportunitystatuses, labelFn: attrLabel('name') +opportunityCloseReasonDropdown → makeEnumDropdown, /opportunityclosereasons, labelFn: attrLabel('label') +``` + +### 4.6 Update Opportunity — `update-opportunity.ts` + +Same as Create but all optional + `opportunityId` required. Follow Update Customer pattern. + +--- + +## Phase 5: Activities & Accounts (5 actions) + +### 5.1 Create Account — `create-account.ts` + +**API endpoint:** `POST /admin/api/accounts` +**JSON:API type:** `accounts` + +**Props:** + +| Prop Name | Type | Required | +|------------------------|------------------------|----------| +| `name` | `ShortText` | ✅ | +| `externalId` | `ShortText` | No | +| `defaultContact` | `contactDropdown` | No | +| `organization` | `organizationDropdown` | No | +| `owner` | `userDropdown` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +### 5.2 Update Account — `update-account.ts` + +Same as Create but all optional + `accountId` required. + +### 5.3 Create Task — `create-task.ts` + +**API endpoint:** `POST /admin/api/tasks` +**JSON:API type:** `tasks` + +**Props (verify against spec):** + +| Prop Name | Type | Required | +|------------------------|-----------------------------------|---------------| +| `subject` | `ShortText` | ✅ | +| `description` | `LongText` | No | +| `dueDate` | `ShortText` | No — datetime | +| `priority` | `StaticDropdown` or enum dropdown | No | +| `status` | Enum dropdown (`/taskstatuses`) | No | +| `owner` | `userDropdown` | No | +| `organization` | `organizationDropdown` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**⚠️ Verify against spec:** Check `/admin/api/tasks` POST schema and `/admin/api/taskstatuses`, `/admin/api/taskpriorities` endpoints before implementing. The endpoint names need validation. + +### 5.4 Create Call — `create-call.ts` + +**API endpoint:** `POST /admin/api/calls` +**JSON:API type:** `calls` + +**Props (verify against spec):** + +| Prop Name | Type | Required | +|------------------------|------------------------|------------------------------| +| `subject` | `ShortText` | ✅ | +| `phoneNumber` | `ShortText` | No | +| `notes` | `LongText` | No | +| `callDateTime` | `ShortText` | No — datetime | +| `duration` | `Number` | No — seconds or formatted | +| `direction` | `StaticDropdown` | No — `incoming` / `outgoing` | +| `owner` | `userDropdown` | No | +| `organization` | `organizationDropdown` | No | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +**⚠️ Verify against spec:** Check `/admin/api/calls` POST schema. The spec shows `calls` has POST support. Verify exact attribute names and required fields. + +### 5.5 Create Calendar Event — `create-calendar-event.ts` + +**⚠️ Verify against spec:** Search for `calendarevents` or `calendar` paths in the spec. Calendar events may use a different path pattern. Verify availability before implementing. + +**Likely props:** + +| Prop Name | Type | Required | +|------------------------|-------------|--------------| +| `title` | `ShortText` | ✅ | +| `description` | `LongText` | No | +| `start` | `ShortText` | ✅ — datetime | +| `end` | `ShortText` | ✅ — datetime | +| `allDay` | `Checkbox` | No | +| `calendar` | Dropdown | ✅ | +| `additionalAttributes` | Existing | No | +| `additionalRelations` | Existing | No | + +--- + +## Summary of All New Dropdown Props + +Add these to `props.ts` between the `customerUserRoleDropdown` section and the `additionalAttributesProp` section, following the existing code style: + +| Export Name | Builder | URI | Fields | Search | Label | +|----------------------------------|--------------------------|-----------------------------|--------------------------------------|-----------------|----------------------| +| `attributeFamilyDropdown` | `makeSearchableDropdown` | `/attributefamilies` | `id,code` | `code ~ "q"` | `attrLabel('code')` | +| `productTaxCodeDropdown` | `makeSearchableDropdown` | `/producttaxcodes` | `id,code` | `code ~ "q"` | `attrLabel('code')` | +| `inventoryStatusDropdown` | `makeEnumDropdown` | `/productinventorystatuses` | — | — | `attrLabel('name')` | +| `categoryDropdown` | `makeSearchableDropdown` | `/mastercatalogcategories` | `id,title` | `title ~ "q"` | `attrLabel('title')` | +| `brandDropdown` | `makeSearchableDropdown` | `/brands` | `id` | `allText ~ "q"` | `(item) => item.id` | +| `contactGroupDropdown` | `makeSearchableDropdown` | `/contactgroups` | `id,label` | `label ~ "q"` | `attrLabel('label')` | +| `contactSourceDropdown` | `makeEnumDropdown` | `/contactsources` | — | — | `attrLabel('label')` | +| `contactMethodDropdown` | `makeEnumDropdown` | `/contactmethods` | — | — | `attrLabel('label')` | +| `contactDropdown` | `makeSearchableDropdown` | `/contacts` | `id,firstName,lastName,primaryEmail` | `allText ~ "q"` | composite fn | +| `leadStatusDropdown` | `makeEnumDropdown` | `/leadstatuses` | — | — | `attrLabel('name')` | +| `leadSourceDropdown` | `makeEnumDropdown` | `/leadsources` | — | — | `attrLabel('name')` | +| `accountDropdown` | `makeSearchableDropdown` | `/accounts` | `id,name` | `name ~ "q"` | `attrLabel('name')` | +| `leadDropdown` | `makeSearchableDropdown` | `/leads` | `id,name` | `name ~ "q"` | `attrLabel('name')` | +| `opportunityStatusDropdown` | `makeEnumDropdown` | `/opportunitystatuses` | — | — | `attrLabel('name')` | +| `opportunityCloseReasonDropdown` | `makeEnumDropdown` | `/opportunityclosereasons` | — | — | `attrLabel('label')` | +| `quoteInternalStatusDropdown` | `makeEnumDropdown` | `/quoteinternalstatuses` | — | — | `attrLabel('name')` | +| `quoteCustomerStatusDropdown` | `makeEnumDropdown` | `/quotecustomerstatuses` | — | — | `attrLabel('name')` | +| `opportunityDropdown` | `makeSearchableDropdown` | `/opportunities` | `id,name` | `name ~ "q"` | `attrLabel('name')` | +| `priceListDropdown` | `makeSearchableDropdown` | `/pricelists` | `id,name` | `name ~ "q"` | `attrLabel('name')` | + +--- + +## Final Checklist Per Action File + +For **every** new action file: + +1. [ ] Create `src/lib/actions/.ts` +2. [ ] Add export in `src/lib/actions/index.ts` +3. [ ] Add import + array entry in `src/index.ts` +4. [ ] Follow exact pattern: imports → createAction → helper functions → types (at end of file) +5. [ ] Use `jsonApiBodyUtils.pickDefined()` for optional attributes +6. [ ] Use `jsonApiBodyUtils.buildRels()` for optional relationships +7. [ ] Always include `additionalAttributes` + `additionalRelations` as last two props +8. [ ] Parse extras with `jsonApiBodyUtils.parseAdditionalAttributes()` / `parseAdditionalRelations()` +9. [ ] Spread extras into attributes/relationships with `...extraAttrs` / `...extraRels` +10. [ ] Return `response.body` from `run()` +11. [ ] Run `npx turbo run lint --filter=orocommerce` after each phase + +--- + +## Spec Verification Commands + +Before implementing Phase 5, verify these endpoints exist in the spec: + +```bash +# Tasks +jq '.paths["/admin/api/tasks"].post.summary' /home/dkhrysev/Downloads/specification.json +jq -r '.paths | keys[] | select(test("taskstatus|taskpriori"; "i"))' /home/dkhrysev/Downloads/specification.json + +# Calls +jq '.paths["/admin/api/calls"].post.summary' /home/dkhrysev/Downloads/specification.json + +# Calendar events +jq -r '.paths | keys[] | select(test("calendar"; "i"))' /home/dkhrysev/Downloads/specification.json + +# For each entity, get complete schema: +# jq '.components.schemas..properties.attributes.properties | keys' /home/dkhrysev/Downloads/specification.json +``` + diff --git a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts index 6c119e8f1342..bde3319ad9b7 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -1,9 +1,11 @@ import { createCustomApiCallAction } from '@activepieces/pieces-common'; import { getAccessToken, + getConnectionHeaders, + getInternalInfrastructureHeaders, getOroAdminApiBaseUrl, oroAuth, - tryCatchSync, + toHeaderRecord, } from '../common'; export const customApiCallAction = createCustomApiCallAction({ @@ -12,17 +14,12 @@ export const customApiCallAction = createCustomApiCallAction({ displayName: 'Custom API Call', description: 'Make a direct authenticated call to the OroCommerce JSON:API.', baseUrl: (auth) => auth ? getOroAdminApiBaseUrl({ auth }) : '', - authMapping: async (auth) => { - const { data } = tryCatchSync(() => JSON.parse(auth.props.headers ?? '{}') as unknown); - const connectionHeaders = - typeof data === 'object' && data !== null && !Array.isArray(data) - ? Object.fromEntries(Object.entries(data as Record).map(([k, v]) => [k, String(v)])) - : {}; - return { - Authorization: `Bearer ${await getAccessToken({ auth })}`, - ...connectionHeaders, - }; - }, + authMapping: async (auth, propsValue: Record) => ({ + ...getConnectionHeaders({ auth }), + ...getInternalInfrastructureHeaders({ auth }), + ...toHeaderRecord({ value: propsValue['headers'] }), + Authorization: `Bearer ${await getAccessToken({ auth })}`, + }), props: { headers: { defaultValue: { diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts index 31d918b40364..0ab06489eb2d 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -4,19 +4,20 @@ import { oroAuth, oroApiCall, customerRequiredDropdown, - customerUserRoleDropdown, + customerUserRolesMultiDropdown, organizationDropdown, userDropdown, websiteDropdown, baseAddressArrayItemProps, addressTypeProps, buildIncludedAddress, + toAddressRow, additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const createCustomerUserAction = createAction({ auth: oroAuth, @@ -88,7 +89,7 @@ export const createCustomerUserAction = createAction({ }), // -- Optional relationships ------------------------------------------------ - userRoles: customerUserRoleDropdown, + userRoles: customerUserRolesMultiDropdown, owner: userDropdown, organization: organizationDropdown, @@ -115,17 +116,22 @@ export const createCustomerUserAction = createAction({ async run(context) { const p = context.propsValue; - const included: Record[] = []; - - const rawAddresses = (p.addresses ?? []) as Array>; - const addressRelData = rawAddresses.map((addr, index) => { + const addresses = (p.addresses ?? []).flatMap((row, index) => { const lid = `cu_addr_${index + 1}`; - included.push( - buildIncludedAddress({ lid, type: 'customeruseraddresses', addr }) - ); - return { type: 'customeruseraddresses', id: lid }; + const resource = buildIncludedAddress({ + lid, + type: 'customeruseraddresses', + addr: toAddressRow(row), + }); + return resource ? [{ lid, resource }] : []; }); + const included = addresses.map(({ resource }) => resource); + const addressRelData = addresses.map(({ lid }) => ({ + type: 'customeruseraddresses', + id: lid, + })); + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts index 177f16a6d671..0e8bc5fd83ac 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts @@ -13,12 +13,13 @@ import { baseAddressArrayItemProps, addressTypeProps, buildIncludedAddress, + toAddressRow, additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const createCustomerAction = createAction({ auth: oroAuth, @@ -82,17 +83,22 @@ export const createCustomerAction = createAction({ async run(context) { const p = context.propsValue; - const included: Record[] = []; - - const rawAddresses = (p.addresses ?? []) as Array>; - const addressRelData = rawAddresses.map((addr, index) => { + const addresses = (p.addresses ?? []).flatMap((row, index) => { const lid = `addr_${index + 1}`; - included.push( - buildIncludedAddress({ lid, type: 'customeraddresses', addr }) - ); - return { type: 'customeraddresses', id: lid }; + const resource = buildIncludedAddress({ + lid, + type: 'customeraddresses', + addr: toAddressRow(row), + }); + return resource ? [{ lid, resource }] : []; }); + const included = addresses.map(({ resource }) => resource); + const addressRelData = addresses.map(({ lid }) => ({ + type: 'customeraddresses', + id: lid, + })); + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index cd8041f9f6e4..1276c0706cd5 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -13,7 +13,7 @@ import { additionalRelationsProp, additionalHeadersProp, } from '../common'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const createInvoiceAction = createAction({ auth: oroAuth, @@ -239,6 +239,8 @@ export const createInvoiceAction = createAction({ customerName: p.customerName, totalAmount: p.totalAmount, ...jsonApiBodyUtils.pickDefined({ + refCustomerId: p.refCustomerId, + refCustomerUserId: p.refCustomerUserId, invoiceNumber: p.invoiceNumber, title: p.title, description: p.description, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index b48cd5cdf6bb..e03e5ec3ad89 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -3,6 +3,8 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { oroAuth, oroApiCall, + attrLabel, + loadDropdownOptions, customerRequiredDropdown, customerUserDropdown, organizationDropdown, @@ -17,12 +19,12 @@ import { buildRegionDropdown, productDropdown, buildIncludedAddress, + type AddressRow, additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, } from '../common'; -import { OroAuth } from '../common/types'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const createOrderAction = createAction({ auth: oroAuth, @@ -30,8 +32,8 @@ export const createOrderAction = createAction({ displayName: 'Create Order', description: 'Creates a new order record in OroCommerce.', props: { - // -- Required relationships ------------------------------------------------ customer: customerRequiredDropdown, + customerUser: customerUserDropdown(false), // -- Optional attributes --------------------------------------------------- currency: Property.ShortText({ @@ -67,12 +69,14 @@ export const createOrderAction = createAction({ }), estimatedShippingCostAmount: Property.Number({ displayName: 'Estimated Shipping Cost', - description: 'Shipping cost calculated from the selected shipping method.', + description: + 'Shipping cost calculated from the selected shipping method.', required: false, }), shippingMethod: Property.ShortText({ displayName: 'Shipping Method', - description: 'The shipping method selected for the order (e.g. "flat_rate_2").', + description: + 'The shipping method selected for the order (e.g. "flat_rate_2").', required: false, }), shippingMethodType: Property.ShortText({ @@ -87,7 +91,6 @@ export const createOrderAction = createAction({ }), // -- Optional relationships ------------------------------------------------ - customerUser: customerUserDropdown(false), organization: organizationDropdown, owner: userDropdown, website: websiteDropdown, @@ -135,7 +138,10 @@ export const createOrderAction = createAction({ displayName: 'Billing: Postal Code', required: false, }), - billingAddressCountry: buildCountryDropdown({ required: false, displayName: 'Billing: Country' }), + billingAddressCountry: buildCountryDropdown({ + required: false, + displayName: 'Billing: Country', + }), billingAddressRegion: buildRegionDropdown({ countryRefresher: 'billingAddressCountry', required: false, @@ -185,7 +191,10 @@ export const createOrderAction = createAction({ displayName: 'Shipping: Postal Code', required: false, }), - shippingAddressCountry: buildCountryDropdown({ required: false, displayName: 'Shipping: Country' }), + shippingAddressCountry: buildCountryDropdown({ + required: false, + displayName: 'Shipping: Country', + }), shippingAddressRegion: buildRegionDropdown({ countryRefresher: 'shippingAddressCountry', required: false, @@ -197,9 +206,6 @@ export const createOrderAction = createAction({ required: false, }), - // -- Line Items ------------------------------------------------------------ - // DynamicProperties lets us load product units once as StaticDropdown. - // Products are entered as SKU/ID text (framework does not support Dropdown inside Array). lineItems: Property.DynamicProperties({ auth: oroAuth, displayName: 'Line Items', @@ -207,51 +213,21 @@ export const createOrderAction = createAction({ required: true, refreshers: [], props: async ({ auth }) => { - type JsonApiCollection = { - data: { id: string; attributes: Record }[]; - }; - - const unitOptions: { label: string; value: string }[] = []; - const warehouseOptions: { label: string; value: string }[] = []; - - if (auth) { - try { - const unitsResp = await oroApiCall({ - method: HttpMethod.GET, - resourceUri: '/productunits', - auth: auth as OroAuth, - queryParams: { 'page[size]': '100' }, - }); - for (const item of (unitsResp.body as JsonApiCollection).data ?? []) { - unitOptions.push({ - label: String(item.attributes['label'] || item.id), - value: item.id, - }); - } - } catch { - // Silently degrade so the form renders even if the catalog API is unavailable - } - - try { - const warehousesResp = await oroApiCall({ - method: HttpMethod.GET, - resourceUri: '/warehouses', - auth: auth as OroAuth, - queryParams: { - 'page[size]': '100', - 'fields[warehouses]': 'id,name', - }, - }); - for (const item of (warehousesResp.body as JsonApiCollection).data ?? []) { - warehouseOptions.push({ - label: String(item.attributes['name'] || item.id), - value: item.id, - }); - } - } catch { - // Silently degrade so the form renders even if the catalog API is unavailable - } - } + const [unitState, warehouseState] = await Promise.all([ + loadDropdownOptions({ + auth, + resourceUri: '/productunits', + labelFn: attrLabel('label'), + exhaustive: true, + }), + loadDropdownOptions({ + auth, + resourceUri: '/warehouses', + labelFn: attrLabel('name'), + fieldsParam: 'id,name', + exhaustive: true, + }), + ]); return { lineItems: Property.Array({ @@ -288,7 +264,7 @@ export const createOrderAction = createAction({ displayName: 'Product Unit', description: 'Unit of measurement (e.g. piece, kg, set).', required: true, - options: { disabled: false, options: unitOptions }, + options: unitState, }), quantity: Property.Number({ displayName: 'Quantity', @@ -344,7 +320,7 @@ export const createOrderAction = createAction({ displayName: 'Warehouse', description: 'Warehouse this line item ships from.', required: false, - options: { disabled: false, options: warehouseOptions }, + options: warehouseState, }), }, }), @@ -402,14 +378,22 @@ export const createOrderAction = createAction({ // -- Line items ---------------------------------------------------------- // DynamicProperties wraps the array in an object keyed by "lineItems" const dynamicValue = (p.lineItems ?? {}) as Record; - const rawItems = (dynamicValue['lineItems'] ?? []) as Array>; + const rawItems = (dynamicValue['lineItems'] ?? []) as Array< + Record + >; - const lineItems = rawItems.map((item, index) => buildOrderLineItem({ item, index, orderCurrency: p.currency })); + const lineItems = rawItems.map((item, index) => + buildOrderLineItem({ item, index, orderCurrency: p.currency }) + ); const lineItemResources = lineItems.map((li) => li.resource); const lineItemsRelData = lineItems.map((li) => li.ref); - const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); - const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes( + p.additionalAttributes + ); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations( + p.additionalRelations + ); // -- Order attributes ---------------------------------------------------- const attributes: Record = { @@ -432,10 +416,18 @@ export const createOrderAction = createAction({ const relationships: Record = { lineItems: { data: lineItemsRelData }, ...(billingAddressResource - ? { billingAddress: { data: { type: 'orderaddresses', id: 'billing_address' } } } + ? { + billingAddress: { + data: { type: 'orderaddresses', id: 'billing_address' }, + }, + } : {}), ...(shippingAddressResource - ? { shippingAddress: { data: { type: 'orderaddresses', id: 'shipping_address' } } } + ? { + shippingAddress: { + data: { type: 'orderaddresses', id: 'shipping_address' }, + }, + } : {}), ...jsonApiBodyUtils.buildRels({ customer: ['customers', p.customer], @@ -466,7 +458,7 @@ export const createOrderAction = createAction({ data: { type: 'orders', attributes, - relationships + relationships, }, included, }, @@ -482,35 +474,30 @@ function buildAddressResource({ fields, }: { localId: string; - fields: { - label?: string | null; - namePrefix?: string | null; - firstName?: string | null; - middleName?: string | null; - lastName?: string | null; - nameSuffix?: string | null; - organization?: string | null; - phone?: string | null; - street?: string | null; - street2?: string | null; - city?: string | null; - postalCode?: string | null; - country?: string | null; - region?: string | null; - customRegion?: string | null; - }; + fields: AddressRow; }): Record | null { - const hasData = Object.values(fields).some((v) => v != null && v !== ''); - if (!hasData) return null; - return buildIncludedAddress({ lid: localId, type: 'orderaddresses', - addr: fields as Record, + addr: fields, extraAttributes: { fromExternalSource: false }, }); } +function toOrderLineItemRow(item: Record): OrderLineItemRow { + const row: OrderLineItemRow = {}; + for (const field of LINE_ITEM_TEXT_FIELDS) { + const value = item[field]; + if (typeof value === 'string') row[field] = value; + } + for (const field of LINE_ITEM_NUMBER_FIELDS) { + const value = item[field]; + if (typeof value === 'number') row[field] = value; + else if (typeof value === 'string') row[field] = Number(value); + } + return row; +} + function buildOrderLineItem({ item, index, @@ -521,52 +508,67 @@ function buildOrderLineItem({ orderCurrency: string | null | undefined; }): { resource: Record; ref: { type: string; id: string } } { const lid = `li_${index + 1}`; - const productUnitId = item['productUnit'] as string | undefined; + const row = toOrderLineItemRow(item); const attributes: Record = { fromExternalSource: true, - productSku: String(item['productSku'] ?? ''), - quantity: Number(item['quantity']), - value: Number(item['value']), - currency: - (item['currency'] as string | undefined) || orderCurrency || 'USD', - priceType: item['priceType'] != null ? Number(item['priceType']) : 10, + productSku: row.productSku ?? '', + quantity: Number(row.quantity), + value: Number(row.value), + currency: row.currency || orderCurrency || 'USD', + priceType: row.priceType ?? 10, ...jsonApiBodyUtils.pickDefined({ - productName: item['productName'] as string | undefined, - freeFormProduct: item['freeFormProduct'] as string | undefined, - comment: item['comment'] as string | undefined, - shipBy: item['shipBy'] as string | undefined, + productName: row.productName, + freeFormProduct: row.freeFormProduct, + comment: row.comment, + shipBy: row.shipBy, }), - ...(item['shippingEstimateAmount'] != null - ? { shippingEstimateAmount: Number(item['shippingEstimateAmount']) } + ...(row.shippingEstimateAmount != null + ? { shippingEstimateAmount: row.shippingEstimateAmount } : {}), - ...(item['shippingMethod'] - ? { shippingMethod: item['shippingMethod'] } - : {}), + ...(row.shippingMethod ? { shippingMethod: row.shippingMethod } : {}), - ...(item['shippingMethodType'] - ? { shippingMethodType: item['shippingMethodType'] } + ...(row.shippingMethodType + ? { shippingMethodType: row.shippingMethodType } : {}), }; - const productId = (item['productId'] as string | undefined)?.trim(); - const liWarehouseId = - (item['warehouseId'] as string | undefined)?.trim() || - (item['warehouse'] as string | undefined)?.trim(); - - const relationships: Record = { - productUnit: { data: { type: 'productunits', id: String(productUnitId) } }, - ...jsonApiBodyUtils.buildRels({ - product: ['products', productId], - warehouse: ['warehouses', liWarehouseId], - }), - }; + const relationships: Record = jsonApiBodyUtils.buildRels({ + productUnit: ['productunits', row.productUnit], + product: ['products', row.productId?.trim()], + warehouse: ['warehouses', row.warehouseId?.trim() || row.warehouse?.trim()], + }); return { resource: { type: 'orderlineitems', id: lid, attributes, relationships }, ref: { type: 'orderlineitems', id: lid }, }; } + +const LINE_ITEM_TEXT_FIELDS = [ + 'productId', + 'productName', + 'freeFormProduct', + 'productSku', + 'productUnit', + 'currency', + 'comment', + 'shipBy', + 'shippingMethod', + 'shippingMethodType', + 'warehouse', + 'warehouseId', +] as const; + +const LINE_ITEM_NUMBER_FIELDS = [ + 'quantity', + 'value', + 'priceType', + 'shippingEstimateAmount', +] as const; + +type OrderLineItemRow = Partial> & + Partial>; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts index 43ac155f2c78..90fc7b6f7ee3 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts @@ -4,18 +4,18 @@ import { oroAuth, oroApiCall, businessUnitRequiredDropdown, - businessUnitDropdown, organizationDropdown, - organizationsDropdown, - userRoleDropdown, - userGroupDropdown, userAuthStatusDropdown, additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + userRolesMultiDropdown, + userGroupsMultiDropdown, + organizationsMultiDropdown, + businessUnitsMultiDropdown, } from '../common'; import { OroAuth } from '../common/types'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const createUserAction = createAction({ auth: oroAuth, @@ -36,7 +36,8 @@ export const createUserAction = createAction({ }), password: Property.ShortText({ displayName: 'Password', - description: 'Password for the new account. Must comply with the system security policy.', + description: + 'Password for the new account. Must comply with the system security policy.', required: true, }), firstName: Property.ShortText({ @@ -89,10 +90,10 @@ export const createUserAction = createAction({ // -- Optional relationships ------------------------------------------------ organization: organizationDropdown, - businessUnits: businessUnitDropdown, - userRoles: userRoleDropdown, - organizations: organizationsDropdown, - groups: userGroupDropdown, + businessUnits: businessUnitsMultiDropdown, + userRoles: userRolesMultiDropdown, + organizations: organizationsMultiDropdown, + groups: userGroupsMultiDropdown, authStatus: userAuthStatusDropdown, additionalAttributes: additionalAttributesProp, @@ -103,8 +104,12 @@ export const createUserAction = createAction({ async run(context) { const p = context.propsValue; - const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); - const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes( + p.additionalAttributes + ); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations( + p.additionalRelations + ); const attributes = { username: p.username, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts b/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts index bf58877097b2..123070c9fa8a 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts @@ -1,62 +1,48 @@ import { createAction, Property } from '@activepieces/pieces-framework'; -import { serialize, type FlatResource, type Linkage, type JsonApiResource } from '../common/jsonapi'; +import { + deserialize, + serialize, + type FlatResource, + type Linkage, + type JsonApiResource, +} from '../common/jsonapi'; export const serializeJsonApiAction = createAction({ name: 'serialize_jsonapi', displayName: 'Serialize JSON:API Request', description: - 'Converts a plain object into a JSON:API-compliant request body. ' + - 'Relationship fields are detected automatically - any array or object value is treated as a ' + - 'relationship, only scalars become attributes. ' + - 'The result can be passed directly as the Request Body of the API Call action.', + 'Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.', auth: undefined, props: { resourceType: Property.ShortText({ displayName: 'Resource Type', - description: - 'The JSON:API resource type (e.g. orders, invoices, products). ' + - 'The _type resolved as resource type automatically.', + description: 'e.g. orders, invoices, products. Taken from _type when left empty.', required: false, }), resourceId: Property.ShortText({ displayName: 'Resource ID', - description: - 'ID of the resource - leave empty when creating a new record (POST) ' + - 'or when the input includes id field (the id field is picked up automatically).', + description: 'Leave empty to create a record, or when the input carries an id.', required: false, }), attributes: Property.Json({ displayName: 'Attributes', description: - 'The flat object to serialize. Pass the full JSON ' + - 'document here - arrays and objects are automatically placed into ' + - '"relationships"; only scalar values become attributes. ' + - 'For manual construction use plain key/value pairs, e.g. ' + - '{"currency":"USD","poNumber":"PO-001"}.', + 'A flat object, Unserialize output, or a single-resource JSON:API document. ' + + 'Values marked with _type or shaped like {"type","id"} become relationships.', required: true, defaultValue: {}, }), relationships: Property.Json({ displayName: 'Relationships (override)', description: - 'Optional explicit relationship map that takes priority over any ' + - 'relationships detected inside Attributes. ' + - 'Each key is a relationship name; the value is a single linkage object ' + - '{"type":"customers","id":"42"}, an array of linkage objects, or null. ' + - 'Example: {"customer":{"type":"customers","id":"42"},' + - '"lineItems":[{"type":"orderlineitems","id":"1"}]}', + 'Wins over anything detected in Attributes. Example: {"customer":{"type":"customers","id":"42"}}', required: false, defaultValue: {}, }), included: Property.Json({ displayName: 'Included', description: - 'Optional array of full JSON:API resource objects to embed in the ' + - '"included" section of the request. If the input to Attributes already ' + - 'contains an "included" array (e.g. the raw API Call response body) it ' + - 'will be forwarded automatically - this field only needs to be set when ' + - 'adding or overriding included resources manually. Each entry must be a ' + - 'valid JSON:API resource with "type", "id", and "attributes" fields.', + 'Extra resources to embed. Forwarded automatically when Attributes already has "included".', required: false, defaultValue: [], }), @@ -65,23 +51,18 @@ export const serializeJsonApiAction = createAction({ async run(context) { const { resourceType, resourceId, attributes, relationships, included } = context.propsValue; - const flat = (attributes as FlatResource) ?? {}; + const input: FlatResource = attributes ?? {}; - // If the caller passed a full JSON:API document that contains an `included` - // array at the top level, extract and forward it automatically. - const docIncluded = Array.isArray((flat as Record)['included']) - ? ((flat as Record)['included'] as unknown as JsonApiResource[]) - : []; - const explicitIncluded = Array.isArray(included) - ? (included as unknown as JsonApiResource[]) - : []; + const docIncluded = toResourceArray(input['included']); + const explicitIncluded = toResourceArray(included); const mergedIncluded = explicitIncluded.length > 0 ? explicitIncluded : docIncluded; - // Fall back to _type embedded in the flat object (Unserialize action output) + const flat = toFlatResource(input); + const resolvedType = (resourceType && resourceType.trim() !== '' ? resourceType.trim() : undefined) ?? (typeof flat['_type'] === 'string' && flat['_type'].trim() !== '' - ? (flat['_type'] as string).trim() + ? flat['_type'].trim() : undefined); if (!resolvedType) { @@ -100,3 +81,45 @@ export const serializeJsonApiAction = createAction({ }); }, }); + +function isResource(value: unknown): value is JsonApiResource { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + 'type' in value && + typeof value['type'] === 'string' && + 'id' in value && + typeof value['id'] === 'string' + ); +} + +function toResourceArray(value: unknown): JsonApiResource[] { + return Array.isArray(value) ? value.filter(isResource) : []; +} + +function toFlatResource(input: FlatResource): FlatResource { + if (!('data' in input)) { + return Object.fromEntries(Object.entries(input).filter(([key]) => key !== 'included')); + } + + const data = input['data']; + + if (Array.isArray(data)) { + throw new Error( + `The input is a collection of ${data.length} resources - its "data" is an array. ` + + 'Serialize JSON:API Request builds a single resource document. Loop over the items and ' + + 'serialize them one at a time, or select a single element (e.g. body.data[0]) first.' + ); + } + + if (!isResource(data)) { + throw new Error( + 'The input has a "data" key, so it is read as a JSON:API document, but its value is not a ' + + 'resource object with string "type" and "id" fields. Pass either a single-resource ' + + 'document ({"data":{"type":"…","id":"…", …}}) or a flat object with no "data" key.' + ); + } + + return deserialize({ data }); +} diff --git a/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts b/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts index 7a9d4adf4e5d..22553e9ee38a 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts @@ -5,18 +5,12 @@ export const unserializeJsonApiAction = createAction({ name: 'unserialize_jsonapi', displayName: 'Unserialize JSON:API Response', description: - 'Flattens a JSON:API response body into a plain object (or array of objects). ' + - 'Included relationships are resolved and inlined. Unresolved relationship ' + - 'references are kept as { _type, id } stubs so the document can be ' + - 're-serialized back to a valid JSON:API request without data loss. ' + - 'Pass the "body" output of the API Call action as the Response input.', + 'Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.', auth: undefined, props: { response: Property.Json({ displayName: 'JSON:API Response', - description: - 'The raw JSON:API response body - the "body" field from the API Call action output. ' + - 'Must contain a top-level "data" key (single resource or collection).', + description: 'The "body" output of the API Call action. Needs a top-level "data" key.', required: true, defaultValue: {}, }), diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index 407c870cbf32..7efa4b05f405 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -4,7 +4,7 @@ import { oroAuth, oroApiCall, customerDropdown, - customerUserRoleDropdown, + customerUserRolesMultiDropdown, organizationDropdown, userDropdown, websiteDropdown, @@ -13,7 +13,7 @@ import { additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const updateCustomerUserAction = createAction({ auth: oroAuth, @@ -88,7 +88,7 @@ export const updateCustomerUserAction = createAction({ // -- Relationships --------------------------------------------------------- customer: customerDropdown, website: websiteDropdown, - userRoles: customerUserRoleDropdown, + userRoles: customerUserRolesMultiDropdown, owner: userDropdown, organization: organizationDropdown, additionalAttributes: additionalAttributesProp, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts index 60f4fd3bf8b8..8b9214a68ef9 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts @@ -15,7 +15,7 @@ import { additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const updateCustomerAction = createAction({ auth: oroAuth, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts index a7fd45eeacaa..0e82c2631805 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -4,17 +4,18 @@ import { oroAuth, oroApiCall, businessUnitDropdown, + businessUnitsMultiDropdown, organizationDropdown, - organizationsDropdown, - userRoleDropdown, - userGroupDropdown, + organizationsMultiDropdown, + userRolesMultiDropdown, + userGroupsMultiDropdown, userAuthStatusDropdown, additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, } from '../common'; import { OroAuth } from '../common/types'; -import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; +import { jsonApiBodyUtils } from '../common/jsonapi'; export const updateUserAction = createAction({ auth: oroAuth, @@ -91,10 +92,10 @@ export const updateUserAction = createAction({ // -- Relationships --------------------------------------------------------- owner: businessUnitDropdown, organization: organizationDropdown, - businessUnits: businessUnitDropdown, - userRoles: userRoleDropdown, - organizations: organizationsDropdown, - groups: userGroupDropdown, + businessUnits: businessUnitsMultiDropdown, + userRoles: userRolesMultiDropdown, + organizations: organizationsMultiDropdown, + groups: userGroupsMultiDropdown, authStatus: userAuthStatusDropdown, additionalAttributes: additionalAttributesProp, diff --git a/packages/pieces/community/orocommerce/src/lib/common/address-props.ts b/packages/pieces/community/orocommerce/src/lib/common/address-props.ts index 0740f97e26e6..adf273faa5bc 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/address-props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/address-props.ts @@ -1,5 +1,19 @@ import { Property } from '@activepieces/pieces-framework'; +export function toAddressRow(row: unknown): AddressRow { + const addr: AddressRow = {}; + if (!isRecord(row)) return addr; + for (const field of [...ADDRESS_TEXT_FIELDS, ...ADDRESS_REF_FIELDS]) { + const value = row[field]; + if (typeof value === 'string') addr[field] = value; + } + for (const field of ADDRESS_FLAG_FIELDS) { + const value = row[field]; + if (typeof value === 'boolean') addr[field] = value; + } + return addr; +} + export function buildIncludedAddress({ lid, type, @@ -8,45 +22,30 @@ export function buildIncludedAddress({ }: { lid: string; type: string; - addr: Record; + addr: AddressRow; extraAttributes?: Record; -}): Record { +}): Record | null { + const hasData = [...ADDRESS_TEXT_FIELDS, ...ADDRESS_REF_FIELDS].some( + (f) => addr[f] != null && addr[f] !== '' + ); + if (!hasData) return null; + const attrs: Record = {}; const rels: Record = {}; - const textFields = [ - 'label', - 'namePrefix', - 'firstName', - 'middleName', - 'lastName', - 'nameSuffix', - 'organization', - 'phone', - 'street', - 'street2', - 'city', - 'postalCode', - 'customRegion', - ]; - for (const f of textFields) { + for (const f of ADDRESS_TEXT_FIELDS) { if (addr[f]) attrs[f] = addr[f]; } - if (addr['primary'] != null) attrs['primary'] = addr['primary']; + if (addr.primary != null) attrs['primary'] = addr.primary; - const types = buildAddressTypes({ - typesBilling: addr['typesBilling'] as boolean | undefined, - typesShipping: addr['typesShipping'] as boolean | undefined, - defaultBilling: addr['defaultBilling'] as boolean | undefined, - defaultShipping: addr['defaultShipping'] as boolean | undefined, - }); + const types = buildAddressTypes(addr); if (types.length > 0) attrs['types'] = types; - if (addr['country']) { - rels['country'] = { data: { type: 'countries', id: addr['country'] } }; + if (addr.country) { + rels['country'] = { data: { type: 'countries', id: addr.country } }; } - if (addr['region']) { - rels['region'] = { data: { type: 'regions', id: addr['region'] } }; + if (addr.region) { + rels['region'] = { data: { type: 'regions', id: addr.region } }; } return { type, id: lid, attributes: { ...extraAttributes, ...attrs }, relationships: rels }; @@ -57,18 +56,17 @@ function buildAddressTypes({ typesShipping, defaultBilling, defaultShipping, -}: { - typesBilling?: boolean; - typesShipping?: boolean; - defaultBilling?: boolean; - defaultShipping?: boolean; -}): Array<{ addressType: string; default: boolean }> { +}: AddressRow): Array<{ addressType: string; default: boolean }> { return [ ...(typesBilling ? [{ addressType: 'billing', default: defaultBilling === true }] : []), ...(typesShipping ? [{ addressType: 'shipping', default: defaultShipping === true }] : []), ]; } +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + /** * Core address fields for use inside `Property.Array`. * Auth-bound dropdowns are not permitted in array item properties, so @@ -173,4 +171,33 @@ export const addressTypeProps = { }), }; +const ADDRESS_TEXT_FIELDS = [ + 'label', + 'namePrefix', + 'firstName', + 'middleName', + 'lastName', + 'nameSuffix', + 'organization', + 'phone', + 'street', + 'street2', + 'city', + 'postalCode', + 'customRegion', +] as const; + +const ADDRESS_REF_FIELDS = ['country', 'region'] as const; + +const ADDRESS_FLAG_FIELDS = [ + 'primary', + 'typesBilling', + 'typesShipping', + 'defaultBilling', + 'defaultShipping', +] as const; +export type AddressRow = Partial< + Record<(typeof ADDRESS_TEXT_FIELDS)[number] | (typeof ADDRESS_REF_FIELDS)[number], string | null> +> & + Partial>; diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 0b59f662a5c5..971163776ede 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -45,8 +45,9 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. headers: Property.LongText({ displayName: 'Default HTTP Headers', description: - 'JSON object of HTTP headers sent with every API request. ' + - 'Actions can override or extend these per-call. ' + + 'JSON object of HTTP headers sent with every action of this piece, including Custom API Call. ' + + 'A header set on the step itself wins over the same header set here. ' + + 'Authorization is always managed by this connection and cannot be overridden per step. ' + 'Example: {"Content-Type": "application/vnd.api+json", "X-Custom": "value"}', required: false }), diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index ffb04c1df766..054ce4e24083 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -1,3 +1,5 @@ +import { createHash } from 'node:crypto'; + import { httpClient, HttpMethod, @@ -6,6 +8,7 @@ import { HttpError, AuthenticationType, } from '@activepieces/pieces-common'; +import { tryCatch } from '@activepieces/pieces-framework'; import { type OroAuth, @@ -15,16 +18,19 @@ import { type OroJsonApiCollection, type FetchCollectionParams, } from './types'; -import { jsonApiBodyUtils } from './jsonapi-body-utils'; -import { tryCatchSync } from './try-catch'; +import { jsonApiBodyUtils } from './jsonapi'; const tokenCache = new Map(); +const inFlightTokenRequests = new Map>(); +// Hashing the secret too keeps two connections that share a server URL and client id from swapping tokens. function buildCacheKey({ auth }: { auth: OroAuth }): string { - return `${getOroServerUrl(auth)}::${auth.props.clientId}`; + return createHash('sha256') + .update([getOroServerUrl(auth), auth.props.clientId, auth.props.clientSecret].join('\0')) + .digest('hex'); } -function formatError({ error }: { error: unknown }): string { +export function formatError({ error }: { error: unknown }): string { if (error instanceof HttpError) { const status = error.response.status; const body = error.response.body; @@ -52,7 +58,7 @@ function isInternalInfrastructure({ auth }: { auth: OroAuth }): boolean { return auth.props.isInternalInfrastructure; } -function getInternalInfrastructureHeaders({ auth }: { auth: OroAuth }): Record { +export function getInternalInfrastructureHeaders({ auth }: { auth: OroAuth }): Record { if (!isInternalInfrastructure({ auth })) { return {}; } @@ -78,28 +84,37 @@ export async function getAccessToken({ auth }: { auth: OroAuth }): Promise({ - method: HttpMethod.POST, - url: `${baseUrl}/oauth2-token`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - ...getInternalInfrastructureHeaders({ auth }), - }, - body: new URLSearchParams({ - grant_type: 'client_credentials', - client_id: auth.props.clientId, - client_secret: auth.props.clientSecret, - }).toString(), - }); + const inFlight = inFlightTokenRequests.get(cacheKey); + if (inFlight) { + return inFlight; + } - const token = response.body.access_token; - tokenCache.set(cacheKey, { - token, - expiresAt: Date.now() + response.body.expires_in * 1000 - 30_000, + const request = requestAccessToken({ auth, cacheKey }).finally(() => { + inFlightTokenRequests.delete(cacheKey); }); + inFlightTokenRequests.set(cacheKey, request); - return token; + return request; +} + +export function getConnectionHeaders({ auth }: { auth: OroAuth }): Record { + const raw = auth.props.headers; + if (!raw) { + return {}; + } + try { + return toHeaderRecord({ value: JSON.parse(raw) }); + } catch { + return {}; + } +} + +export function toHeaderRecord({ value }: { value: unknown }): Record { + if (!isRecord(value)) { + return {}; + } + + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, String(item)])); } export async function oroApiCall({ @@ -109,30 +124,44 @@ export async function oroApiCall({ queryParams, body, headers: extraHeaders, + throwOriginalError = false }: OroApiCallParams): Promise> { - try { - const resource = resourceUri.replace(/^\/+/, ''); - const sanitizedBody = sanitizeJsonApiBody({ body }); - const connectionHeaders = parseConnectionHeaders({ raw: auth.props.headers }); - - return await httpClient.sendRequest({ + const sendRequest = async ({ token }: { token: string }): Promise> => + await httpClient.sendRequest({ method, - url: `${getOroAdminApiBaseUrl({ auth })}/${resource}`, + url: `${getOroAdminApiBaseUrl({ auth })}/${resourceUri.replace(/^\/+/, '')}`, headers: { 'Content-Type': 'application/vnd.api+json', - ...connectionHeaders, + ...getConnectionHeaders({ auth }), ...getInternalInfrastructureHeaders({ auth }), ...extraHeaders, }, authentication: { type: AuthenticationType.BEARER_TOKEN, - token: await getAccessToken({ auth }), + token, }, queryParams, - body: sanitizedBody, + body: sanitizeJsonApiBody({ body }), }); + + try { + const token = await getAccessToken({ auth }); + const { data, error } = await tryCatch(() => sendRequest({ token })); + if (!error) { + return data; + } + if (!(error instanceof HttpError) || error.response.status !== 401) { + throw error; + } + invalidateAccessToken({ auth, token }); + + return await sendRequest({ token: await getAccessToken({ auth }) }); } catch (error: unknown) { - throw new Error(formatError({ error })); + if (throwOriginalError) { + throw error; + } else { + throw new Error(formatError({ error })); + } } } @@ -157,25 +186,59 @@ function sanitizeJsonApiBody({ body }: { body: unknown }): unknown { return body; } const record = body as Record; - if (!('data' in record) || typeof record['data'] !== 'object' || record['data'] === null) { - return body; + const { included, ...withoutIncluded } = record; + const sanitized = + Array.isArray(included) && included.length === 0 ? withoutIncluded : record; + if ( + !('data' in sanitized) || + typeof sanitized['data'] !== 'object' || + sanitized['data'] === null + ) { + return sanitized; } return { - ...record, - data: jsonApiBodyUtils.omitEmptyObjects(record['data'] as Record), + ...sanitized, + data: jsonApiBodyUtils.omitEmptyObjects(sanitized['data'] as Record), }; } -function toStringRecord({ obj }: { obj: Record | undefined }): Record { - if (!obj) return {}; - return Object.fromEntries( - Object.entries(obj).map(([k, v]) => [k, String(v)]), - ); +async function requestAccessToken({ + auth, + cacheKey, +}: { + auth: OroAuth; + cacheKey: string; +}): Promise { + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `${getOroServerUrl(auth)}/oauth2-token`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...getInternalInfrastructureHeaders({ auth }), + }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: auth.props.clientId, + client_secret: auth.props.clientSecret, + }).toString(), + }); + + const token = response.body.access_token; + tokenCache.set(cacheKey, { + token, + expiresAt: Date.now() + response.body.expires_in * 1000 - 30_000, + }); + + return token; +} + +function invalidateAccessToken({ auth, token }: { auth: OroAuth; token: string }): void { + const cacheKey = buildCacheKey({ auth }); + if (tokenCache.get(cacheKey)?.token === token) { + tokenCache.delete(cacheKey); + } } -function parseConnectionHeaders({ raw }: { raw: string | null | undefined }): Record { - if (!raw) return {}; - const { data, error } = tryCatchSync(() => JSON.parse(raw) as unknown); - if (error || typeof data !== 'object' || data === null || Array.isArray(data)) return {}; - return toStringRecord({ obj: data as Record }); +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); } diff --git a/packages/pieces/community/orocommerce/src/lib/common/index.ts b/packages/pieces/community/orocommerce/src/lib/common/index.ts index 4a007e9de1c6..b48c0715d530 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -2,5 +2,4 @@ export * from './auth'; export * from './client'; export * from './props'; export * from './address-props'; -export * from './jsonapi-body-utils'; -export * from './try-catch'; +export * from './jsonapi'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts similarity index 100% rename from packages/pieces/community/orocommerce/src/lib/common/jsonapi-body-utils.ts rename to packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts index 817402b36134..df2881cdd4ec 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts @@ -1,13 +1,11 @@ -import type { JsonApiDocument, JsonApiResource, Linkage, FlatResource, DeserializeResult } from './types'; - -/** - * Sentinel value written by the deserializer for a null to-one relationship. - * Carrying `_type: null` lets the serializer distinguish it from a plain - * null attribute and round-trip it back to `{ data: null }`. - */ -export const NULL_RELATIONSHIP: FlatResource = Object.freeze({ _type: null, id: null }); - -// -- internal helpers ------------------------------------------------------- +import type { + JsonApiDocument, + JsonApiResource, + JsonApiResourceDocument, + Linkage, + FlatResource, + DeserializeResult, +} from './types'; function buildIndex(included: JsonApiResource[]): Map { const m = new Map(); @@ -21,7 +19,6 @@ function resolveRef( visited: Set, ): FlatResource { const key = `${ref.type}:${ref.id}`; - // cycle guard or not in included -> minimal restorable stub if (visited.has(key)) return { _type: ref.type, id: ref.id }; const resource = index.get(key); if (resource) return flattenResource(resource, index, new Set(visited)); @@ -38,13 +35,14 @@ function flattenResource( for (const [name, rel] of Object.entries(r.relationships ?? {})) { const lnk = rel.data; if (lnk === null || lnk === undefined) { - // null to-one relationship — use sentinel so serializer can restore it out[name] = NULL_RELATIONSHIP; continue; } if (Array.isArray(lnk)) { - // empty array stays as empty array; elements are resolved individually - out[name] = lnk.map((ref) => resolveRef(ref, index, new Set(visited))); + out[name] = + lnk.length === 0 + ? EMPTY_TO_MANY + : lnk.map((ref) => resolveRef(ref, index, new Set(visited))); continue; } out[name] = resolveRef(lnk, index, new Set(visited)); @@ -52,18 +50,9 @@ function flattenResource( return out; } -// -- public API ------------------------------------------------------------- - -/** - * Deserialize a JSON:API document into a plain flat object or array. - * - * - Resources in `included` are resolved and inlined. - * - Relationship references **not** found in `included` become `{ _type, id }` - * stubs instead of `null`, preserving all information needed to re-serialize. - * - Null to-one relationships become `{ _type: null, id: null }` sentinels. - * - Circular references (e.g. order -> lineItem -> order) are broken safely. - */ -export function deserialize(doc: JsonApiDocument): DeserializeResult | unknown { +export function deserialize(doc: JsonApiResourceDocument): FlatResource; +export function deserialize(doc: JsonApiDocument): DeserializeResult | JsonApiDocument; +export function deserialize(doc: JsonApiDocument): DeserializeResult | JsonApiDocument { if (!doc?.data) return doc; const index = buildIndex(doc.included ?? []); if (Array.isArray(doc.data)) { @@ -71,3 +60,9 @@ export function deserialize(doc: JsonApiDocument): DeserializeResult | unknown { } return flattenResource(doc.data, index, new Set()); } + +// Sentinels: the flat shape crosses flow steps as plain JSON, where a null/empty relationship would +// otherwise be indistinguishable from a null/empty attribute and serialize back as one. +export const NULL_RELATIONSHIP: FlatResource = Object.freeze({ _type: null, id: null }); + +export const EMPTY_TO_MANY: FlatResource = Object.freeze({ _emptyToMany: true }); diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts index 1f6b874ee7d5..6bbc242e62e8 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts @@ -1,3 +1,4 @@ export * from './types'; export * from './deserialize'; export * from './serialize'; +export * from './body-utils'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts index 0ea5dc8eac40..69d9ec4e87d9 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts @@ -1,211 +1,212 @@ import type { FlatResource, JsonApiResource, Linkage, SerializeOptions, SerializeResult } from './types'; -// -- internal helpers ------------------------------------------------------- +export function serialize(options: SerializeOptions): SerializeResult { + const { type, id, data: flat, relationships: explicitRels = {}, included } = options; + + const { + attributes: detectedAttrs, + relationships: detectedRels, + hoisted, + } = splitFlat({ flat }); + + const explicitRelNames = new Set(Object.keys(explicitRels)); + + const attributes = Object.fromEntries( + Object.entries(detectedAttrs).filter(([name]) => !explicitRelNames.has(name)) + ); + + const mergedRels: Record = { + ...detectedRels, + ...Object.fromEntries( + Object.entries(explicitRels).map(([name, linkage]) => [name, { data: linkage }]) + ), + }; + + const idFromFlat = + flat['id'] != null && String(flat['id']).trim() !== '' + ? String(flat['id']).trim() + : undefined; + const resolvedId = (id && id.trim() !== '' ? id.trim() : undefined) ?? idFromFlat; + + const dataBlock: Record = { + type, + ...(resolvedId ? { id: resolvedId } : {}), + attributes, + ...(Object.keys(mergedRels).length > 0 ? { relationships: mergedRels } : {}), + }; + + const result: SerializeResult = { data: dataBlock }; + + const explicitIncluded = included ?? []; + const explicitKeys = new Set(explicitIncluded.map((r) => `${r.type}::${r.id}`)); + const mergedIncluded = [ + ...hoisted.filter((r) => !explicitKeys.has(`${r.type}::${r.id}`)), + ...explicitIncluded, + ]; + if (mergedIncluded.length > 0) result.included = mergedIncluded; + + return result; +} -/** - * Returns true when a value is a plain object that carries a non-null `_type` - * marker injected by `deserialize()` — i.e. a resolved or stub relationship. - */ -function hasTypeMarker(v: unknown): v is FlatResource { +function hasTypeMarker(value: unknown): value is MarkedResource { return ( - v !== null && - typeof v === 'object' && - !Array.isArray(v) && - typeof (v as FlatResource)['_type'] === 'string' + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + '_type' in value && + typeof value['_type'] === 'string' ); } -/** - * Returns true when a value is the null-relationship sentinel emitted by - * `deserialize()` for JSON:API `{ "data": null }` relationships. - */ -function isNullRelationship(v: unknown): boolean { +function isNullRelationship(value: unknown): boolean { return ( - v !== null && - typeof v === 'object' && - !Array.isArray(v) && - (v as FlatResource)['_type'] === null + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + '_type' in value && + value['_type'] === null ); } -/** Convert a `_type`-marked flat object back to a JSON:API linkage `{ type, id }`. */ -function toLinkage(obj: FlatResource): Linkage { - return { type: obj['_type'] as string, id: String(obj['id'] ?? '') }; +function isEmptyToMany(value: unknown): boolean { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + '_emptyToMany' in value && + value['_emptyToMany'] === true + ); +} + +function isRawLinkage(value: unknown): value is RawLinkage { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + 'type' in value && + typeof value['type'] === 'string' && + 'id' in value && + value['id'] != null + ); } -/** - * Returns true when a `_type`-marked object is a *full* inlined resource - * (has attributes beyond `_type` and `id`) rather than a bare stub. - * - * A stub produced by `deserialize()` for a relationship that was NOT included - * looks like `{ _type: "customers", id: "5" }` — exactly two keys. - * A full inlined resource has additional attribute / relationship keys. - */ -function isFullResource(v: FlatResource): boolean { - return Object.keys(v).some((k) => k !== '_type' && k !== 'id'); +function isLinkageLike(value: unknown): value is MarkedResource | RawLinkage { + return hasTypeMarker(value) || isRawLinkage(value); } -/** - * Convert a full inlined flat resource into a `JsonApiResource` wire object, - * recursing so that nested inlined objects are also hoisted into `collected`. - * - * De-duplicates by `type + id`: if the same resource has already been added to - * `collected` it is skipped (prevents duplicates when billingAddress and - * shippingAddress point to the same record). - */ -function hoistResource(obj: FlatResource, collected: Map): Linkage { - const linkage = toLinkage(obj); +function isFullResource(resource: MarkedResource): boolean { + return Object.keys(resource).some((k) => k !== '_type' && k !== 'id'); +} + +function toLinkage(resource: MarkedResource): Linkage { + return { type: resource['_type'], id: String(resource['id'] ?? '') }; +} + +function linkOrHoist({ + value, + collected, +}: { + value: MarkedResource | RawLinkage; + collected: Map; +}): Linkage { + if (hasTypeMarker(value)) { + return isFullResource(value) ? hoistResource({ resource: value, collected }) : toLinkage(value); + } + return { type: value.type, id: String(value.id) }; +} + +function hoistResource({ + resource, + collected, +}: { + resource: MarkedResource; + collected: Map; +}): Linkage { + const linkage = toLinkage(resource); const key = `${linkage.type}::${linkage.id}`; if (!collected.has(key)) { - // Reserve the slot immediately to break any circular references - const resource: JsonApiResource = { type: linkage.type, id: linkage.id }; - collected.set(key, resource); - - const attrs: Record = {}; - const rels: Record = {}; - - for (const [k, v] of Object.entries(obj)) { - if (k === '_type' || k === 'id') continue; - - if (isNullRelationship(v)) { - rels[k] = { data: null }; - } else if (Array.isArray(v)) { - rels[k] = { - data: v.map((item) => - hasTypeMarker(item) - ? isFullResource(item) - ? hoistResource(item, collected) - : toLinkage(item) - : (item as Linkage) - ), - }; - } else if (hasTypeMarker(v)) { - rels[k] = { - data: isFullResource(v) ? hoistResource(v, collected) : toLinkage(v), - }; - } else if (v !== null && typeof v === 'object') { - // plain object without _type — treat as attribute - attrs[k] = v; - } else { - attrs[k] = v; - } - } + const hoisted: JsonApiResource = { type: linkage.type, id: linkage.id }; + collected.set(key, hoisted); - if (Object.keys(attrs).length > 0) resource.attributes = attrs; - if (Object.keys(rels).length > 0) resource.relationships = rels; + const { attributes, relationships } = splitEntries({ flat: resource, collected }); + if (Object.keys(attributes).length > 0) hoisted.attributes = attributes; + if (Object.keys(relationships).length > 0) hoisted.relationships = relationships; } return linkage; } -/** - * Split a flat (deserialized) object into JSON:API `attributes`, - * auto-detected `relationships`, and `autoIncluded` resources. - * - * Rules (applied after skipping `_type` and `id`): - * - Scalar (string | number | boolean | null) -> `attributes`. - * - `{ _type: null }` sentinel -> relationship `{ data: null }`. - * - Plain object with `_type` and **only** `_type`+`id` keys (stub) - * -> to-one relationship linkage, nothing added to included. - * - Plain object with `_type` and **extra** fields (full inlined resource) - * -> to-one relationship linkage **+** object hoisted into `autoIncluded`. - * - Array -> to-many relationship; each element follows the same stub/full rule. - */ -function splitFlat(flat: FlatResource): { - attributes: FlatResource; - relationships: Record; - autoIncluded: JsonApiResource[]; -} { +function toManyBlock({ + name, + items, + collected, +}: { + name: string; + items: unknown[]; + collected: Map; +}): RelationshipBlock | undefined { + const linkageLike = items.filter(isLinkageLike); + if (linkageLike.length === 0) return undefined; + + if (linkageLike.length !== items.length) { + const index = items.findIndex((item) => !isLinkageLike(item)); + throw new Error( + `Property "${name}" mixes relationship linkages with plain values (offending element at index ${index}). ` + + 'A to-many relationship must contain only linkage objects — either {"type":"…","id":"…"} ' + + 'or values carrying a _type marker. Move plain values to a separate attribute.' + ); + } + + return { data: linkageLike.map((value) => linkOrHoist({ value, collected })) }; +} + +function splitEntries({ + flat, + collected, +}: { + flat: FlatResource; + collected: Map; +}): { attributes: FlatResource; relationships: Record } { const attributes: FlatResource = {}; - const relationships: Record = {}; - const collected = new Map(); + const relationships: Record = {}; for (const [key, value] of Object.entries(flat)) { if (key === '_type' || key === 'id') continue; - // null-relationship sentinel if (isNullRelationship(value)) { relationships[key] = { data: null }; - continue; - } - - // any array -> to-many relationship - if (Array.isArray(value)) { - relationships[key] = { - data: value.map((item) => { - if (!hasTypeMarker(item)) return item; - return isFullResource(item) ? hoistResource(item, collected) : toLinkage(item); - }), - }; - continue; - } - - // any plain object with _type -> to-one relationship - if (hasTypeMarker(value)) { - relationships[key] = { - data: isFullResource(value) ? hoistResource(value, collected) : toLinkage(value), - }; - continue; - } - - // plain object without _type -> to-one relationship, passed as-is - if (value !== null && typeof value === 'object') { - relationships[key] = { data: value }; - continue; + } else if (isEmptyToMany(value)) { + relationships[key] = { data: [] }; + } else if (Array.isArray(value)) { + const block = toManyBlock({ name: key, items: value, collected }); + if (block) { + relationships[key] = block; + } else { + attributes[key] = value; + } + } else if (isLinkageLike(value)) { + relationships[key] = { data: linkOrHoist({ value, collected }) }; + } else { + attributes[key] = value; } - - // scalar (string, number, boolean, null) -> attribute - attributes[key] = value; } - return { attributes, relationships, autoIncluded: Array.from(collected.values()) }; + return { attributes, relationships }; } -// -- public API ------------------------------------------------------------- - -/** - * Serialize a flat object into a JSON:API-compliant document. - * - * - Scalar fields -> `attributes`; arrays and objects -> `relationships`. - * - Null-relationship sentinels (`{ _type: null }`) -> `{ data: null }`. - * - Explicit `relationships` in options take priority over auto-detected ones. - * - `id` is read from options first, then from the flat object's own `id` field. - * - If `included` resources are provided they are forwarded into the document. - */ -export function serialize(options: SerializeOptions): SerializeResult { - const { type, id, data: flat, relationships: explicitRels = {}, included } = options; - - const { attributes, relationships: detectedRels, autoIncluded } = splitFlat(flat); - - // Explicit relationships override auto-detected ones - const mergedRels: Record = { ...detectedRels }; - for (const [name, linkage] of Object.entries(explicitRels)) { - mergedRels[name] = { data: linkage }; - } - - // id: explicit option wins over id embedded in the flat object - const idFromFlat = - flat['id'] != null && String(flat['id']).trim() !== '' - ? String(flat['id']).trim() - : undefined; - const resolvedId = (id && id.trim() !== '' ? id.trim() : undefined) ?? idFromFlat; - - const dataBlock: Record = { type, attributes }; - if (resolvedId) dataBlock['id'] = resolvedId; - if (Object.keys(mergedRels).length > 0) dataBlock['relationships'] = mergedRels; +function splitFlat({ flat }: { flat: FlatResource }): { + attributes: FlatResource; + relationships: Record; + hoisted: JsonApiResource[]; +} { + const collected = new Map(); + const { attributes, relationships } = splitEntries({ flat, collected }); + return { attributes, relationships, hoisted: Array.from(collected.values()) }; +} - const result: SerializeResult = { data: dataBlock }; +type MarkedResource = FlatResource & { _type: string }; - // Merge auto-hoisted resources with explicitly provided included. - // Explicit entries win over auto-hoisted ones with the same type+id. - const explicitIncluded = included ?? []; - const explicitKeys = new Set(explicitIncluded.map((r) => `${r.type}::${r.id}`)); - const mergedIncluded = [ - ...autoIncluded.filter((r) => !explicitKeys.has(`${r.type}::${r.id}`)), - ...explicitIncluded, - ]; - if (mergedIncluded.length > 0) result.included = mergedIncluded; +type RawLinkage = { type: string; id: unknown }; - return result; -} +type RelationshipBlock = { data: Linkage | Linkage[] | null }; diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts index bd037b7c55bd..bf3384309ad3 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts @@ -1,5 +1,3 @@ -// -- JSON:API wire types ---------------------------------------------------- - export type Linkage = { type: string; id: string }; export interface JsonApiResource { @@ -14,7 +12,10 @@ export interface JsonApiDocument { included?: JsonApiResource[]; } -// -- Simplified / flat output types ---------------------------------------- +export interface JsonApiResourceDocument { + data: JsonApiResource; + included?: JsonApiResource[]; +} /** * A plain object produced by deserializing a JSON:API resource. diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index db95f35623dd..b44c74940c97 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -1,11 +1,9 @@ -import { Property } from '@activepieces/pieces-framework'; +import { DropdownState, Property, tryCatch } from '@activepieces/pieces-framework'; import { HttpMethod } from '@activepieces/pieces-common'; import { oroAuth } from './auth'; import { oroApiCall, fetchCollection } from './client'; import { OroAuth, OroJsonApiItem } from './types'; -// --- Shared sentinels --------------------------------------------------------- - const NOT_CONNECTED = { disabled: true, placeholder: 'Connect your OroCommerce account first', @@ -14,17 +12,20 @@ const NOT_CONNECTED = { const FAILED = { disabled: true, - placeholder: 'Failed to load options. Check your connection.', + placeholder: + 'Could not load the list. Check the connection and its permissions for this resource.', options: [], }; -// --- Dropdown builders -------------------------------------------------------- +const PAGE_SIZE = 100; + +const MAX_PAGES = 20; function sanitizeSearch(value: string): string { return value.trim().replaceAll('"', ''); } -function attrLabel(...attrs: string[]): LabelFn { +export function attrLabel(...attrs: string[]): LabelFn { return (item) => String( attrs.reduce( @@ -53,6 +54,91 @@ function clientSideFilterAndSort({ .sort((a, b) => a.label.localeCompare(b.label)); } +export async function loadDropdownOptions({ + auth, + resourceUri, + labelFn, + fieldsParam, + queryParams = {}, + exhaustive = false, +}: { + auth: OroAuth | undefined; + resourceUri: string; + labelFn: LabelFn; + fieldsParam?: string; + queryParams?: Record; + exhaustive?: boolean; +}): Promise> { + if (!auth) return NOT_CONNECTED; + + const allParams = { + ...(fieldsParam + ? { [`fields[${resourceUri.slice(1)}]`]: fieldsParam } + : {}), + ...queryParams, + }; + + const { data, error } = await tryCatch(() => + exhaustive + ? fetchEveryPage({ auth, resourceUri, queryParams: allParams }) + : fetchFirstPage({ auth, resourceUri, queryParams: allParams }) + ); + if (error) return FAILED; + + const options = data.items.map((item) => ({ + label: labelFn(item), + value: item.id, + })); + + return data.complete + ? { options } + : { + options, + placeholder: `Showing the first ${options.length} records only - more exist but are not listed`, + }; +} + +async function fetchFirstPage({ + auth, + resourceUri, + queryParams, +}: { + auth: OroAuth; + resourceUri: string; + queryParams: Record; +}): Promise { + const items = await fetchCollection({ auth, resourceUri, queryParams }); + return { items, complete: true }; +} + +async function fetchEveryPage({ + auth, + resourceUri, + queryParams, +}: { + auth: OroAuth; + resourceUri: string; + queryParams: Record; +}): Promise { + const pages: OroJsonApiItem[][] = []; + for (let pageNumber = 1; pageNumber <= MAX_PAGES; pageNumber++) { + const page = await fetchCollection({ + auth, + resourceUri, + queryParams: { + ...queryParams, + 'page[size]': String(PAGE_SIZE), + 'page[number]': String(pageNumber), + }, + }); + pages.push(page); + if (page.length < PAGE_SIZE) { + return { items: pages.flat(), complete: true }; + } + } + return { items: pages.flat(), complete: false }; +} + function makeSearchableDropdown({ displayName, description, @@ -71,35 +157,54 @@ function makeSearchableDropdown({ required, refreshers, refreshOnSearch: true, - options: async ({ auth }, { searchValue }) => { - if (!auth) return NOT_CONNECTED; - try { - const params: Record = { - [`fields[${resourceUri.slice(1)}]`]: fieldsParam, + options: ({ auth }, { searchValue }) => { + const trimmed = searchValue?.trim() ?? ''; + return loadDropdownOptions({ + auth, + resourceUri, + fieldsParam, + labelFn, + queryParams: { ...extraParams, - }; - const trimmed = searchValue?.trim() ?? ''; - if (trimmed.length > 0) { - params['filter[searchQuery]'] = searchExpr(sanitizeSearch(trimmed)); - } - const items = await fetchCollection({ - auth: auth as OroAuth, - resourceUri, - queryParams: params, - }); - return { - options: items.map((item) => ({ - label: labelFn(item), - value: item.id, - })), - }; - } catch { - return FAILED; - } + ...(trimmed.length > 0 + ? { 'filter[searchQuery]': searchExpr(sanitizeSearch(trimmed)) } + : {}), + }, + }); }, }); } +function makeMultiSelectDropdown({ + displayName, + description, + required = false, + refreshers = [], + resourceUri, + fieldsParam, + labelFn, + extraParams = {}, +}: MultiSelectDropdownConfig) { + // No refreshOnSearch: the multi-select widget stores selections as indexes into the current + // options array, so a server-side search would re-map already-picked values onto other records. + return Property.MultiSelectDropdown({ + auth: oroAuth, + displayName, + description, + required, + refreshers, + options: ({ auth }) => + loadDropdownOptions({ + auth, + resourceUri, + fieldsParam, + labelFn, + queryParams: extraParams, + exhaustive: true, + }), + }); +} + function makeEnumDropdown({ displayName, description, @@ -114,24 +219,14 @@ function makeEnumDropdown({ description, required, refreshers: [], - options: async ({ auth }) => { - if (!auth) return NOT_CONNECTED; - try { - const items = await fetchCollection({ - auth: auth as OroAuth, - resourceUri, - queryParams: extraParams, - }); - return { - options: items.map((item) => ({ - label: labelFn(item), - value: item.id, - })), - }; - } catch { - return FAILED; - } - }, + options: ({ auth }) => + loadDropdownOptions({ + auth, + resourceUri, + labelFn, + queryParams: extraParams, + exhaustive: true, + }), }); } @@ -288,7 +383,7 @@ export const productDropdown = Property.Dropdown({ const params: Record = { 'fields[products]': 'id,sku,status,names', include: 'names', - 'fields[productnames]': 'id,string,localization', + 'fields[productnames]': 'id,string,localization,product', }; const trimmed = searchValue?.trim() ?? ''; if (trimmed.length > 0) { @@ -389,9 +484,6 @@ export const buildCountryDropdown = ( }, }); -// --- Regions ------------------------------------------------------------------ -// Loads all regions for the selected country once and filters client-side. - export const buildRegionDropdown = ({ countryRefresher, required = false, @@ -408,34 +500,26 @@ export const buildRegionDropdown = ({ 'Region or state. Select a country first. Start typing to filter.', required, refreshers: [countryRefresher], - options: async (context) => { - const auth = context['auth'] as OroAuth | undefined; - const countryId = context[countryRefresher as keyof typeof context] as - | string - | undefined; + options: async ({ auth, ...refreshed }) => { + const countryId = refreshed[countryRefresher]; if (!auth) return NOT_CONNECTED; - if (!countryId) + if (typeof countryId !== 'string' || countryId.length === 0) return { disabled: true, placeholder: 'Select a country first', options: [], }; - try { - const items = await fetchCollection({ - auth, - resourceUri: '/regions', - queryParams: { 'filter[country]': countryId }, - }); - return { - options: clientSideFilterAndSort({ - items, - searchValue: undefined, - labelAttr: 'name', - }), - }; - } catch { - return FAILED; - } + const state = await loadDropdownOptions({ + auth, + resourceUri: '/regions', + labelFn: attrLabel('name'), + queryParams: { 'filter[country]': countryId }, + exhaustive: true, + }); + return { + ...state, + options: [...state.options].sort((a, b) => a.label.localeCompare(b.label)), + }; }, }); @@ -467,7 +551,6 @@ export const productUnitDropdown = makeEnumDropdown({ description: 'Unit of measure for the product (e.g. each, set, kg).', resourceUri: '/productunits', labelFn: attrLabel('label', 'code'), - extraParams: { 'page[size]': '100' }, }); // --- Customer Groups ---------------------------------------------------------- @@ -492,6 +575,15 @@ export const businessUnitDropdown = makeSearchableDropdown({ labelFn: attrLabel('name'), }); +export const businessUnitsMultiDropdown = makeMultiSelectDropdown({ + displayName: 'Business Units (replaces all existing business units)', + description: + 'The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.', + resourceUri: '/businessunits', + fieldsParam: 'id,name', + labelFn: attrLabel('name'), +}); + export const businessUnitRequiredDropdown = makeSearchableDropdown({ displayName: 'Owner (Business Unit)', description: 'The business unit that owns this record.', @@ -513,6 +605,15 @@ export const userRoleDropdown = makeSearchableDropdown({ labelFn: attrLabel('label', 'role'), }); +export const userRolesMultiDropdown = makeMultiSelectDropdown({ + displayName: 'User Roles (replaces all existing roles)', + description: + 'The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.', + resourceUri: '/userroles', + fieldsParam: 'id,label,role', + labelFn: attrLabel('label', 'role'), +}); + // --- User Groups -------------------------------------------------------------- export const userGroupDropdown = makeSearchableDropdown({ @@ -524,6 +625,15 @@ export const userGroupDropdown = makeSearchableDropdown({ labelFn: attrLabel('name'), }); +export const userGroupsMultiDropdown = makeMultiSelectDropdown({ + displayName: 'User Groups (replaces all existing groups)', + description: + 'The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.', + resourceUri: '/usergroups', + fieldsParam: 'id,name', + labelFn: attrLabel('name'), +}); + // --- Organizations (multi) ---------------------------------------------------- export const organizationsDropdown = makeSearchableDropdown({ @@ -535,6 +645,15 @@ export const organizationsDropdown = makeSearchableDropdown({ labelFn: attrLabel('name'), }); +export const organizationsMultiDropdown = makeMultiSelectDropdown({ + displayName: 'Organizations (replaces all existing organizations)', + description: + 'The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.', + resourceUri: '/organizations', + fieldsParam: 'id,name', + labelFn: attrLabel('name'), +}); + // --- User Auth Statuses ------------------------------------------------------- export const userAuthStatusDropdown = makeEnumDropdown({ @@ -576,15 +695,20 @@ export const customerUserRoleDropdown = makeSearchableDropdown({ labelFn: attrLabel('label', 'role'), }); +export const customerUserRolesMultiDropdown = makeMultiSelectDropdown({ + displayName: 'Roles (replaces all existing roles)', + description: + 'The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.', + resourceUri: '/customeruserroles', + fieldsParam: 'id,label,role', + labelFn: attrLabel('label', 'role'), +}); + // --- Additional Attributes / Relations (custom entity fields) ----------------- export const additionalAttributesProp = Property.Json({ displayName: 'Additional Attributes', - description: - 'Optional JSON object with custom entity attributes. ' + - 'Keys are attribute names, values are their values. ' + - 'Example: {"myCustomField": "value", "priority": 5}. ' + - 'These are merged into the request body after the standard fields.', + description: 'Custom fields, merged after the standard ones. Example: {"myField": "value"}', required: false, defaultValue: {}, }); @@ -592,21 +716,14 @@ export const additionalAttributesProp = Property.Json({ export const additionalRelationsProp = Property.Json({ displayName: 'Additional Relations', description: - 'Optional JSON object with custom entity relationships. ' + - 'Each key is a relationship name and the value must follow JSON:API linkage format. ' + - 'Example: {"myRelation": {"data": {"type": "myentities", "id": "1"}}, ' + - '"myMultiRelation": {"data": [{"type": "otherentities", "id": "2"}]}}. ' + - 'These are merged into the request body after the standard relationships.', + 'Custom relationships in linkage format. Example: {"myRelation": {"data": {"type": "myentities", "id": "1"}}}', required: false, defaultValue: {}, }); export const additionalHeadersProp = Property.Object({ displayName: 'Additional Headers', - description: - 'Optional HTTP headers to send with this request. ' + - 'These override or extend the default headers configured on the connection. ' + - 'Example: {"X-Include": "noHateoas;totalCount"}.', + description: 'Headers for this step; override the connection defaults. Example: {"X-Include": "totalCount"}', required: false, defaultValue: {}, }); @@ -640,6 +757,11 @@ function buildProductNameMap( type LabelFn = (item: OroJsonApiItem) => string; +type CollectionPages = { + items: OroJsonApiItem[]; + complete: boolean; +}; + type SearchableDropdownConfig = { displayName: string; description: string; @@ -652,6 +774,8 @@ type SearchableDropdownConfig = { extraParams?: Record; }; +type MultiSelectDropdownConfig = Omit; + type EnumDropdownConfig = { displayName: string; description: string; diff --git a/packages/pieces/community/orocommerce/src/lib/common/try-catch.ts b/packages/pieces/community/orocommerce/src/lib/common/try-catch.ts deleted file mode 100644 index 36c24eb7426f..000000000000 --- a/packages/pieces/community/orocommerce/src/lib/common/try-catch.ts +++ /dev/null @@ -1,21 +0,0 @@ -// copied from not-allowed to import activepieces/try-catch -type Success = { - data: T; - error: null; -}; - -type Failure = { - data: null; - error: E; -}; - -type Result = Success | Failure; - -export function tryCatchSync(fn: () => T): Result { - try { - const data = fn(); - return { data, error: null }; - } catch (error) { - return { data: null, error: error as E }; - } -} diff --git a/packages/pieces/community/orocommerce/src/lib/common/types.ts b/packages/pieces/community/orocommerce/src/lib/common/types.ts index d2a96c03eb1a..b9b071bfde24 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/types.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -23,6 +23,7 @@ export type OroApiCallParams = { queryParams?: QueryParams; body?: HttpMessageBody; headers?: Record; + throwOriginalError?: boolean; }; export type FetchCollectionParams = { diff --git a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts b/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts deleted file mode 100644 index afbb92251bc1..000000000000 --- a/packages/pieces/community/orocommerce/src/lib/examples/invoice.ts +++ /dev/null @@ -1,117 +0,0 @@ -const invoiceData = { - data: { - type: 'invoices', - id: '1', - attributes: { - uuid: 'c194f5ca-8030-4efb-aaae-b4d1eb906b1a', - invoiceNumber: 'INV-2026-02-00001', - invoiceDate: '2026-02-03', - currency: 'USD', - totalAmount: '59.2100', - customerName: 'Customer A', - refCustomerId: null, - refCustomerUserId: null, - title: 'Sample Invoice', - description: 'Sample prepaid service', - memo: 'Thank you for choosing us!', - billTo: '101 Pine St, Ogdenville, USA, 44556', - shipTo: '654 Elm St, Shelbyville, USA, 09876', - shippingMethod: 'International Shipping (Tracking #: 901234)', - sellerInfo: - 'Charlie White, 101 Main St, North Haverbrook, USA, 77889', - externalPaymentUrl: 'https://example.com/pay/123456790', - createdAt: '2026-02-03T11:33:49Z', - updatedAt: '2026-02-18T11:35:37Z', - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: 0, - timesContactedIn: 0, - timesContactedOut: 0, - paymentStatus: { - code: 'refunded', - label: 'Refunded', - isForced: false, - }, - }, - relationships: { - lineItems: { - data: [ - { - type: 'invoicelineitems', - id: '1', - }, - { - type: 'invoicelineitems', - id: '2', - }, - { - type: 'invoicelineitems', - id: '3', - }, - ], - }, - owner: { - data: { - type: 'users', - id: '1', - }, - }, - organization: { - data: { - type: 'organizations', - id: '1', - }, - }, - customer: { - data: null, - }, - customer_user: { - data: null, - }, - website: { - data: null, - }, - invoiceDefaultPdfFile: { - data: null, - }, - orders: { - data: [], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - internal_status: { - data: { - type: 'invoiceinternalstatuses', - id: 'draft', - }, - }, - }, - }, - included: [] -}; - -export const invoiceCreateData = { - topic: 'invoice.create', - eventData: invoiceData, - timestamp: 1771523549, - messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' -}; - -export const invoiceUpdateData = { - topic: 'invoice.update', - eventData: invoiceData, - timestamp: 1771527786, - messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' -}; - -export const invoiceDeleteData = { - topic: 'invoice.delete', - eventData: invoiceData, - timestamp: 1771527301, - messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' -}; diff --git a/packages/pieces/community/orocommerce/src/lib/examples/order.ts b/packages/pieces/community/orocommerce/src/lib/examples/order.ts deleted file mode 100644 index 3ee8f74ab115..000000000000 --- a/packages/pieces/community/orocommerce/src/lib/examples/order.ts +++ /dev/null @@ -1,637 +0,0 @@ -export const orderCreateData = { - topic: 'order.create', - eventData: { - data: { - type: 'orders', - id: '48', - attributes: { - identifier: '48', - poNumber: 'PONUM111222334', - customerNotes: 'Customer Notes', - shipUntil: { - date: '2026-02-27 00:00:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - currency: 'USD', - subtotalWithDiscountsValue: '79.9900', - subtotalWithDiscounts: '79.9900', - subtotalValue: '79.9900', - totalValue: '89.9900', - shippingMethod: 'fixed_product_6', - shippingMethodType: 'primary', - estimatedShippingCostAmount: '10.0000', - overriddenShippingCostAmount: null, - sourceEntityIdentifier: null, - totalDiscountsAmount: null, - external: false, - createdAt: { - date: '2026-02-19 19:40:25.000000', - timezone_type: 3, - timezone: 'UTC', - }, - updatedAt: { - date: '2026-02-19 19:40:25.000000', - timezone_type: 3, - timezone: 'UTC', - }, - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: null, - timesContactedIn: null, - timesContactedOut: null, - disablePromotions: false, - discount: 0, - shippingDiscount: 0, - }, - relationships: { - createdBy: { - data: { - type: 'users', - id: '1', - }, - }, - billingAddress: { - data: { - type: 'orderaddresses', - id: '86', - }, - }, - shippingAddress: { - data: { - type: 'orderaddresses', - id: '87', - }, - }, - website: { - data: { - type: 'websites', - id: '1', - }, - }, - lineItems: { - data: [ - { - type: 'orderlineitems', - id: '129', - }, - ], - }, - discounts: { - data: [], - }, - shippingTrackings: { - data: [], - }, - parent: { - data: null, - }, - subOrders: { - data: [], - }, - owner: { - data: { - type: 'users', - id: '1', - }, - }, - organization: { - data: { - type: 'organizations', - id: '1', - }, - }, - customerUser: { - data: { - type: 'customerusers', - id: '1', - }, - }, - customer: { - data: { - type: 'customers', - id: '5', - }, - }, - paymentTerm: { - data: { - type: 'paymentterms', - id: '1', - }, - }, - invoices: { - data: [], - }, - warehouse: { - data: { - type: 'warehouses', - id: '1', - }, - }, - source: { - data: null, - }, - internalStatus: { - data: { - type: 'orderinternalstatuses', - id: 'order_internal_status.open', - }, - }, - orderSubtotals: { - data: [ - { - type: 'ordersubtotals', - id: '48-subtotal-0', - }, - { - type: 'ordersubtotals', - id: '48-discount-1', - }, - { - type: 'ordersubtotals', - id: '48-shipping_cost-2', - }, - { - type: 'ordersubtotals', - id: '48-discount-3', - }, - { - type: 'ordersubtotals', - id: '48-tax-4', - }, - { - type: 'ordersubtotals', - id: '48-tax-5', - }, - { - type: 'ordersubtotals', - id: '48-tax-6', - }, - ], - }, - attachments: { - data: [], - }, - documents: { - data: [ - { - type: 'files', - id: '661', - meta: { - sortOrder: 1, - }, - }, - ], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - status: { - data: null, - }, - shippingStatus: { - data: { - type: 'ordershippingstatuses', - id: 'order_shipping_status.not_shipped', - }, - }, - }, - }, - included: [] - }, - timestamp: 1771530026, - messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' -}; - -export const orderUpdateData = { - topic: 'order.update', - eventData: { - data: { - type: 'orders', - id: '42', - attributes: { - identifier: '42', - poNumber: 'PONUM111222334', - customerNotes: 'Order Customer Notes', - shipUntil: { - date: '2026-02-28 00:00:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - currency: 'USD', - subtotalWithDiscountsValue: '236.9700', - subtotalWithDiscounts: '236.9700', - subtotalValue: '239.9700', - totalValue: '246.9700', - shippingMethod: 'fixed_product_6', - shippingMethodType: 'primary', - estimatedShippingCostAmount: '10.0000', - overriddenShippingCostAmount: null, - sourceEntityIdentifier: null, - totalDiscountsAmount: '3.0000', - external: false, - createdAt: { - date: '2026-02-19 17:52:28.000000', - timezone_type: 3, - timezone: 'UTC', - }, - updatedAt: { - date: '2026-02-19 19:03:06.000000', - timezone_type: 3, - timezone: 'UTC', - }, - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: null, - timesContactedIn: null, - timesContactedOut: null, - disablePromotions: false, - discount: 0, - shippingDiscount: 0, - }, - relationships: { - createdBy: { - data: { - type: 'users', - id: '1', - }, - }, - billingAddress: { - data: { - type: 'orderaddresses', - id: '74', - }, - }, - shippingAddress: { - data: { - type: 'orderaddresses', - id: '75', - }, - }, - website: { - data: { - type: 'websites', - id: '1', - }, - }, - lineItems: { - data: [ - { - type: 'orderlineitems', - id: '123', - }, - ], - }, - discounts: { - data: [ - { - type: 'orderdiscounts', - id: '1', - }, - ], - }, - shippingTrackings: { - data: [], - }, - parent: { - data: null, - }, - subOrders: { - data: [], - }, - owner: { - data: { - type: 'users', - id: '1', - }, - }, - organization: { - data: { - type: 'organizations', - id: '1', - }, - }, - customerUser: { - data: { - type: 'customerusers', - id: '1', - }, - }, - customer: { - data: { - type: 'customers', - id: '5', - }, - }, - paymentTerm: { - data: { - type: 'paymentterms', - id: '1', - }, - }, - invoices: { - data: [], - }, - warehouse: { - data: { - type: 'warehouses', - id: '1', - }, - }, - source: { - data: null, - }, - internalStatus: { - data: { - type: 'orderinternalstatuses', - id: 'order_internal_status.open', - }, - }, - orderSubtotals: { - data: [ - { - type: 'ordersubtotals', - id: '42-subtotal-0', - }, - { - type: 'ordersubtotals', - id: '42-discount-1', - }, - { - type: 'ordersubtotals', - id: '42-discount-2', - }, - { - type: 'ordersubtotals', - id: '42-shipping_cost-3', - }, - { - type: 'ordersubtotals', - id: '42-discount-4', - }, - { - type: 'ordersubtotals', - id: '42-tax-5', - }, - { - type: 'ordersubtotals', - id: '42-tax-6', - }, - { - type: 'ordersubtotals', - id: '42-tax-7', - }, - ], - }, - attachments: { - data: [], - }, - documents: { - data: [ - { - type: 'files', - id: '656', - meta: { - sortOrder: 1, - }, - }, - ], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - status: { - data: null, - }, - shippingStatus: { - data: { - type: 'ordershippingstatuses', - id: 'order_shipping_status.not_shipped', - }, - }, - }, - }, - included: [], - }, - timestamp: 1771527786, - messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' -}; - -export const orderDeleteData = { - topic: 'order.delete', - eventData: { - data: { - type: 'orders', - id: '20', - attributes: { - identifier: 'FR1012408', - poNumber: 'RT104568EUR', - customerNotes: null, - shipUntil: { - date: '2026-02-18 00:00:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - currency: 'USD', - subtotalWithDiscountsValue: '33951.0000', - subtotalWithDiscounts: '33951.0000', - subtotalValue: '33951.0000', - totalValue: '33951.0000', - shippingMethod: null, - shippingMethodType: null, - estimatedShippingCostAmount: null, - overriddenShippingCostAmount: null, - sourceEntityIdentifier: null, - totalDiscountsAmount: null, - external: false, - createdAt: { - date: '2025-12-19 21:04:00.000000', - timezone_type: 3, - timezone: 'UTC', - }, - updatedAt: { - date: '2026-02-18 11:35:37.000000', - timezone_type: 3, - timezone: 'UTC', - }, - lastContactedDate: null, - lastContactedDateIn: null, - lastContactedDateOut: null, - timesContacted: 0, - timesContactedIn: 0, - timesContactedOut: 0, - disablePromotions: false, - discount: 0, - shippingDiscount: 0, - }, - relationships: { - createdBy: { - data: { - type: 'users', - id: '4', - }, - }, - billingAddress: { - data: { - type: 'orderaddresses', - id: '27', - }, - }, - shippingAddress: { - data: { - type: 'orderaddresses', - id: '28', - }, - }, - website: { - data: { - type: 'websites', - id: '1', - }, - }, - lineItems: { - data: [ - { - type: 'orderlineitems', - id: '65', - }, - { - type: 'orderlineitems', - id: '66', - }, - { - type: 'orderlineitems', - id: '67', - }, - ], - }, - discounts: { - data: [], - }, - shippingTrackings: { - data: [], - }, - parent: { - data: null, - }, - subOrders: { - data: [], - }, - owner: { - data: { - type: 'users', - id: '21', - }, - }, - organization: { - data: { - type: 'organizations', - id: '1', - }, - }, - customerUser: { - data: { - type: 'customerusers', - id: '11', - }, - }, - customer: { - data: { - type: 'customers', - id: '11', - }, - }, - paymentTerm: { - data: { - type: 'paymentterms', - id: '3', - }, - }, - invoices: { - data: [ - { - type: 'invoices', - id: '20', - }, - ], - }, - warehouse: { - data: null, - }, - source: { - data: null, - }, - internalStatus: { - data: { - type: 'orderinternalstatuses', - id: 'order_internal_status.open', - }, - }, - orderSubtotals: { - data: [ - { - type: 'ordersubtotals', - id: '20-subtotal-0', - }, - { - type: 'ordersubtotals', - id: '20-discount-1', - }, - { - type: 'ordersubtotals', - id: '20-shipping_cost-2', - }, - { - type: 'ordersubtotals', - id: '20-discount-3', - }, - { - type: 'ordersubtotals', - id: '20-tax-4', - }, - { - type: 'ordersubtotals', - id: '20-tax-5', - }, - { - type: 'ordersubtotals', - id: '20-tax-6', - }, - ], - }, - attachments: { - data: [], - }, - documents: { - data: [], - }, - activityNotes: { - data: [], - }, - activityEmails: { - data: [], - }, - status: { - data: null, - }, - shippingStatus: { - data: { - type: 'ordershippingstatuses', - id: 'order_shipping_status.not_shipped', - }, - }, - }, - }, - included: [], - }, - timestamp: 1771527301, - messageId: '7206ba9b-c9da-4867-ab5a-c307a59f6381' -}; diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index 7cafbb188c92..557931c2764e 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -3,9 +3,9 @@ import { Property, TriggerStrategy, } from '@activepieces/pieces-framework'; -import { oroAuth, oroApiCall } from '../common'; +import { oroAuth, oroApiCall, formatError } from '../common'; import { OroAuth, OroJsonApiCollection, OroJsonApiItem } from '../common/types'; -import { HttpMethod } from '@activepieces/pieces-common'; +import { HttpError, HttpMethod } from '@activepieces/pieces-common'; export const oroWebhookTopicTrigger = createTrigger({ auth: oroAuth, @@ -92,11 +92,23 @@ export const oroWebhookTopicTrigger = createTrigger({ const webhookInfo = await context.store.get('webhookInfo'); if (webhookInfo !== null && webhookInfo !== undefined) { - await oroApiCall({ - method: HttpMethod.DELETE, - resourceUri: `webhooks/${webhookInfo.webhookId}`, - auth: context.auth, - }); + try { + await oroApiCall({ + method: HttpMethod.DELETE, + resourceUri: `webhooks/${webhookInfo.webhookId}`, + auth: context.auth, + throwOriginalError: true, + }); + } catch (error: unknown) { + if (error instanceof HttpError) { + // Skip already removed webhooks and access denied errors + if ([401, 403, 404].includes(error.response.status)) { + return; + } + } + + throw new Error(formatError({ error })); + } } }, diff --git a/packages/pieces/community/orocommerce/test/jsonapi-roundtrip.test.ts b/packages/pieces/community/orocommerce/test/jsonapi-roundtrip.test.ts new file mode 100644 index 000000000000..2f68519bbb52 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/jsonapi-roundtrip.test.ts @@ -0,0 +1,243 @@ +import { describe, expect, it } from 'vitest'; +import { createMockActionContext } from '@activepieces/pieces-framework'; +import { + deserialize, + serialize, + type FlatResource, + type JsonApiResourceDocument, +} from '../src/lib/common/jsonapi'; +import { serializeJsonApiAction } from '../src/lib/actions/serialize-jsonapi'; + +function throughFlowJson(flat: FlatResource): FlatResource { + return JSON.parse(JSON.stringify(flat)); +} + +function runSerializeAction({ + resourceType, + attributes, +}: { + resourceType?: string; + attributes: FlatResource; +}) { + return serializeJsonApiAction.run( + createMockActionContext({ + propsValue: { + resourceType, + resourceId: undefined, + attributes, + relationships: undefined, + included: undefined, + }, + }) + ); +} + +describe('deserialize -> serialize round trip', () => { + const doc: JsonApiResourceDocument = { + data: { + type: 'customeraddresses', + id: '1', + attributes: { + label: 'Billing', + types: [{ addressType: 'billing', default: true }], + options: { validated: true, source: { channel: 'web' } }, + tags: ['a', 'b'], + emptyList: [], + }, + relationships: { + customer: { data: { type: 'customers', id: '5' } }, + country: { data: null }, + regions: { data: [{ type: 'regions', id: 'US-CA' }] }, + salesRepresentatives: { data: [] }, + }, + }, + }; + + it('keeps object- and array-valued attributes as attributes', () => { + const result = serialize({ type: 'customeraddresses', data: deserialize(doc) }); + + expect(result.data['attributes']).toEqual({ + label: 'Billing', + types: [{ addressType: 'billing', default: true }], + options: { validated: true, source: { channel: 'web' } }, + tags: ['a', 'b'], + emptyList: [], + }); + expect(result.data['id']).toBe('1'); + }); + + it('keeps _type-marked values as relationships', () => { + const result = serialize({ type: 'customeraddresses', data: deserialize(doc) }); + + expect(result.data['relationships']).toEqual({ + customer: { data: { type: 'customers', id: '5' } }, + country: { data: null }, + regions: { data: [{ type: 'regions', id: 'US-CA' }] }, + salesRepresentatives: { data: [] }, + }); + }); + + it('round-trips an empty to-many relationship through the flow JSON boundary', () => { + const source: JsonApiResourceDocument = { + data: { + type: 'customerusers', + id: '7', + attributes: { email: 'a@b.c' }, + relationships: { + userRoles: { data: [] }, + customer: { data: { type: 'customers', id: '5' } }, + }, + }, + }; + + const result = serialize({ + type: 'customerusers', + data: throughFlowJson(deserialize(source)), + }); + + expect(result.data['attributes']).toEqual({ email: 'a@b.c' }); + expect(result.data['relationships']).toEqual({ + userRoles: { data: [] }, + customer: { data: { type: 'customers', id: '5' } }, + }); + }); + + it('hoists included resources back into included', () => { + const flat = deserialize({ + data: { + type: 'orders', + id: '9', + attributes: { currency: 'USD' }, + relationships: { customer: { data: { type: 'customers', id: '5' } } }, + }, + included: [{ type: 'customers', id: '5', attributes: { name: 'Acme' } }], + }); + + const result = serialize({ type: 'orders', data: flat }); + + expect(result.data['relationships']).toEqual({ + customer: { data: { type: 'customers', id: '5' } }, + }); + expect(result.included).toEqual([ + { type: 'customers', id: '5', attributes: { name: 'Acme' } }, + ]); + }); +}); + +describe('serialize classification', () => { + it('keeps a bare empty array as an attribute', () => { + const result = serialize({ type: 'customerusers', data: { email: 'a@b.c', emptyList: [] } }); + + expect(result.data['attributes']).toEqual({ email: 'a@b.c', emptyList: [] }); + expect(result.data['relationships']).toBeUndefined(); + }); + + it('does not duplicate an explicit relationship into attributes', () => { + const result = serialize({ + type: 'customerusers', + data: { email: 'a@b.c', userRoles: [] }, + relationships: { userRoles: [] }, + }); + + expect(result.data['attributes']).toEqual({ email: 'a@b.c' }); + expect(result.data['relationships']).toEqual({ userRoles: { data: [] } }); + }); + + it('treats hand-written raw linkages as relationships', () => { + const result = serialize({ + type: 'orders', + data: { + currency: 'USD', + lineItems: [ + { type: 'orderlineitems', id: '1' }, + { type: 'orderlineitems', id: '2' }, + ], + customer: { type: 'customers', id: '5' }, + }, + }); + + expect(result.data['attributes']).toEqual({ currency: 'USD' }); + expect(result.data['relationships']).toEqual({ + lineItems: { + data: [ + { type: 'orderlineitems', id: '1' }, + { type: 'orderlineitems', id: '2' }, + ], + }, + customer: { data: { type: 'customers', id: '5' } }, + }); + expect(JSON.stringify(result)).not.toContain('_type'); + }); + + it('throws on an array that mixes linkages with plain values', () => { + expect(() => + serialize({ + type: 'orders', + data: { rel: [{ _type: 'a', id: '1' }, { id: '2' }] }, + }) + ).toThrow(/"rel".*index 1/); + }); +}); + +describe('serialize action document unwrapping', () => { + const collection: FlatResource = { + data: [ + { type: 'orders', id: '1', attributes: { currency: 'USD' } }, + { type: 'orders', id: '2', attributes: { currency: 'EUR' } }, + ], + included: [{ type: 'customers', id: '5', attributes: { name: 'Acme' } }], + }; + + it('rejects a collection document instead of returning an empty one', async () => { + await expect(runSerializeAction({ resourceType: 'orders', attributes: collection })).rejects.toThrow( + /collection of 2 resources/ + ); + }); + + it('rejects a collection document even when Resource Type is empty', async () => { + await expect(runSerializeAction({ attributes: collection })).rejects.toThrow( + /collection of 2 resources/ + ); + }); + + it('rejects a document whose data is not a resource object', async () => { + await expect( + runSerializeAction({ resourceType: 'orders', attributes: { data: 'nope' } }) + ).rejects.toThrow(/not a resource object/); + }); + + it('still unwraps a single-resource document', async () => { + const result = await runSerializeAction({ + attributes: { + data: { + type: 'orders', + id: '9', + attributes: { currency: 'USD' }, + relationships: { customer: { data: { type: 'customers', id: '5' } } }, + }, + included: [{ type: 'customers', id: '5', attributes: { name: 'Acme' } }], + }, + }); + + expect(result).toEqual({ + data: { + type: 'orders', + id: '9', + attributes: { currency: 'USD' }, + relationships: { customer: { data: { type: 'customers', id: '5' } } }, + }, + included: [{ type: 'customers', id: '5', attributes: { name: 'Acme' } }], + }); + }); + + it('leaves a flat object without a data key untouched', async () => { + const result = await runSerializeAction({ + resourceType: 'orders', + attributes: { currency: 'USD', poNumber: 'PO-001' }, + }); + + expect(result).toEqual({ + data: { type: 'orders', attributes: { currency: 'USD', poNumber: 'PO-001' } }, + }); + }); +}); diff --git a/packages/pieces/community/orocommerce/tsconfig.json b/packages/pieces/community/orocommerce/tsconfig.json index 29c9dd1bfc1c..07e0ec6eb797 100644 --- a/packages/pieces/community/orocommerce/tsconfig.json +++ b/packages/pieces/community/orocommerce/tsconfig.json @@ -14,6 +14,9 @@ "references": [ { "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" } ] } diff --git a/packages/pieces/community/orocommerce/tsconfig.spec.json b/packages/pieces/community/orocommerce/tsconfig.spec.json new file mode 100644 index 000000000000..1dc71104755b --- /dev/null +++ b/packages/pieces/community/orocommerce/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["vitest/globals", "node"], + "allowSyntheticDefaultImports": true + }, + "include": [ + "vitest.config.ts", + "test/**/*.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/packages/pieces/community/orocommerce/vitest.config.ts b/packages/pieces/community/orocommerce/vitest.config.ts new file mode 100644 index 000000000000..f520fc141133 --- /dev/null +++ b/packages/pieces/community/orocommerce/vitest.config.ts @@ -0,0 +1,18 @@ +import path from 'path' +import { defineConfig } from 'vitest/config' + +const repoRoot = path.resolve(__dirname, '../../../..') + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@activepieces/shared': path.resolve(repoRoot, 'packages/core/shared/src/index.ts'), + '@activepieces/pieces-framework': path.resolve(repoRoot, 'packages/pieces/framework/src/index.ts'), + '@activepieces/pieces-common': path.resolve(repoRoot, 'packages/pieces/common/src/index.ts'), + }, + }, +}) From 198a97814c2a6968c5cd6d526a21b80f4d8b9d0d Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 13 Aug 2026 16:48:50 +0200 Subject: [PATCH 30/50] docs(orocommerce): record what the user actions do to an account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four actions take a password as an ordinary step input. There is no step-level secret property to move them to — SecretTextProperty exists only as a PieceAuthProperty and is deliberately absent from the InputProperty union that createAction's props must satisfy — so the descriptions now say plainly that the value is stored in clear text and visible to anyone who can open the flow. Two behaviours that were only visible by reading the code are now stated where a flow builder will see them: create-customer-user defaults enabled and confirmed to true, so it produces an active account without an email confirmation round trip; update-user can change username, email, password and auth status in one call, which is enough to lock an existing user out. Co-Authored-By: Claude Opus 5 (1M context) --- .../orocommerce/src/lib/actions/create-customer-user.ts | 9 ++++++--- .../community/orocommerce/src/lib/actions/create-user.ts | 2 +- .../orocommerce/src/lib/actions/update-customer-user.ts | 3 ++- .../community/orocommerce/src/lib/actions/update-user.ts | 5 +++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts index 0ab06489eb2d..4ba38b7ffcf8 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -33,7 +33,8 @@ export const createCustomerUserAction = createAction({ }), password: Property.ShortText({ displayName: 'Password', - description: 'Password for the new account. Must comply with the system security policy.', + description: + 'Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', required: true, }), @@ -67,13 +68,15 @@ export const createCustomerUserAction = createAction({ // -- Optional attributes --------------------------------------------------- enabled: Property.Checkbox({ displayName: 'Enabled', - description: 'When disabled the user cannot log into the storefront. Defaults to true.', + description: + 'When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.', required: false, defaultValue: true, }), confirmed: Property.Checkbox({ displayName: 'Confirmed', - description: 'Whether the user has completed email confirmation. Defaults to true.', + description: + 'Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.', required: false, defaultValue: true, }), diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts index 90fc7b6f7ee3..2604341dfd3a 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts @@ -37,7 +37,7 @@ export const createUserAction = createAction({ password: Property.ShortText({ displayName: 'Password', description: - 'Password for the new account. Must comply with the system security policy.', + 'Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', required: true, }), firstName: Property.ShortText({ diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index 7efa4b05f405..42a83874c635 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -37,7 +37,8 @@ export const updateCustomerUserAction = createAction({ }), password: Property.ShortText({ displayName: 'Password', - description: 'New password for the account. Must comply with the system security policy.', + description: + 'New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', required: false, }), diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts index 0e82c2631805..7936f0f3a36c 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -22,7 +22,7 @@ export const updateUserAction = createAction({ name: 'update_user', displayName: 'Update User', description: - 'Updates an existing back-office user in OroCommerce. Only provided fields are changed.', + 'Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.', props: { // -- Target record --------------------------------------------------------- userId: Property.ShortText({ @@ -44,7 +44,8 @@ export const updateUserAction = createAction({ }), password: Property.ShortText({ displayName: 'Password', - description: 'New password for the account. Must comply with the system security policy.', + description: + 'New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', required: false, }), firstName: Property.ShortText({ From 223b6c6fa02ae5cc9a8b85258428ae03618468e2 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 13 Aug 2026 16:48:51 +0200 Subject: [PATCH 31/50] fix(orocommerce): validate line items instead of coercing them with Number() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both order and invoice creation turned user input into numbers with a bare Number(). Number(undefined) is NaN and JSON.stringify serialises NaN as null, so a missing or non-numeric quantity reached Oro as null — no error, no failed run, just a wrong record. The invoice action had no validation at all and passed description through untyped; the order action filtered partially in toOrderLineItemRow and still coerced with Number() afterwards, and fell back to an empty productSku when the required field was absent. Both now go through lineItemUtils, which reports the row number and the field label a flow builder actually sees: Line Items row 3, "Quantity" must be a number, got "abc". Blank and whitespace-only strings count as missing rather than as 0, booleans are rejected rather than becoming 1, a wrong type is reported as a wrong type rather than as a missing value, and text fields are trimmed so an id pasted with whitespace still resolves. Optional fields holding only whitespace are treated as absent, so a stray space does not fail the run. Invoice creation additionally reconciles Total Amount against the sum of the row totals. The comparison is in minor units so 0.1 + 0.2 against 0.3 does not fail, with one cent of tolerance for rounding. The prop description had claimed the total "should equal the sum of all line item row totals" without anything enforcing it. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/actions/create-invoice.ts | 83 +++++- .../src/lib/actions/create-order.ts | 211 ++++++++++----- .../orocommerce/src/lib/common/index.ts | 1 + .../orocommerce/src/lib/common/line-items.ts | 238 +++++++++++++++++ .../orocommerce/test/line-items.test.ts | 242 ++++++++++++++++++ 5 files changed, 697 insertions(+), 78 deletions(-) create mode 100644 packages/pieces/community/orocommerce/src/lib/common/line-items.ts create mode 100644 packages/pieces/community/orocommerce/test/line-items.test.ts diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index 1276c0706cd5..33a7eb553ab7 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -12,6 +12,7 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + lineItemUtils, } from '../common'; import { jsonApiBodyUtils } from '../common/jsonapi'; @@ -189,28 +190,84 @@ export const createInvoiceAction = createAction({ async run(context) { const p = context.propsValue; - const dynamicValue = (p.lineItems ?? {}) as Record; - const rawItems = (dynamicValue['lineItems'] ?? []) as Array< - Record - >; + const rows = lineItemUtils.readRows({ + value: p.lineItems, + arrayKey: 'lineItems', + displayName: LINE_ITEMS_DISPLAY_NAME, + }); - const lineItemResources = rawItems.map((item, index) => ({ + const lineItemResources = rows.map((row, index) => ({ type: 'invoicelineitems', id: `li_${index + 1}`, attributes: { position: index + 1, - lineNumber: item['lineNumber'] || String(index + 1), - description: item['description'], - quantity: Number(item['quantity']), - unitOfQuantity: item['unitOfQuantity'], - unitPrice: Number(item['unitPrice']), - rowTotal: Number(item['rowTotal']), + lineNumber: + lineItemUtils.optionalString({ + row, + index, + field: 'lineNumber', + label: 'Line Number', + displayName: LINE_ITEMS_DISPLAY_NAME, + }) ?? String(index + 1), + description: lineItemUtils.requiredString({ + row, + index, + field: 'description', + label: 'Description', + displayName: LINE_ITEMS_DISPLAY_NAME, + }), + quantity: lineItemUtils.requiredNumber({ + row, + index, + field: 'quantity', + label: 'Quantity', + displayName: LINE_ITEMS_DISPLAY_NAME, + min: 0, + }), + unitPrice: lineItemUtils.requiredNumber({ + row, + index, + field: 'unitPrice', + label: 'Unit Price', + displayName: LINE_ITEMS_DISPLAY_NAME, + min: 0, + }), + rowTotal: lineItemUtils.requiredNumber({ + row, + index, + field: 'rowTotal', + label: 'Row Total', + displayName: LINE_ITEMS_DISPLAY_NAME, + }), ...jsonApiBodyUtils.pickDefined({ - note: item['note'] as string | undefined, + unitOfQuantity: lineItemUtils.optionalString({ + row, + index, + field: 'unitOfQuantity', + label: 'Product Unit', + displayName: LINE_ITEMS_DISPLAY_NAME, + }), + note: lineItemUtils.optionalString({ + row, + index, + field: 'note', + label: 'Note', + displayName: LINE_ITEMS_DISPLAY_NAME, + }), }), }, })); + lineItemUtils.assertSumMatches({ + rows, + field: 'rowTotal', + label: 'Row Total', + displayName: LINE_ITEMS_DISPLAY_NAME, + total: p.totalAmount, + totalLabel: 'Total Amount', + toleranceMinorUnits: 1, + }); + const lineItemsRelData = lineItemResources.map((li) => ({ type: 'invoicelineitems', id: li.id, @@ -289,3 +346,5 @@ export const createInvoiceAction = createAction({ return response.body; }, }); + +const LINE_ITEMS_DISPLAY_NAME = 'Line Items'; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index e03e5ec3ad89..7a88659acbc6 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -23,6 +23,7 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + lineItemUtils, } from '../common'; import { jsonApiBodyUtils } from '../common/jsonapi'; @@ -377,13 +378,14 @@ export const createOrderAction = createAction({ // -- Line items ---------------------------------------------------------- // DynamicProperties wraps the array in an object keyed by "lineItems" - const dynamicValue = (p.lineItems ?? {}) as Record; - const rawItems = (dynamicValue['lineItems'] ?? []) as Array< - Record - >; + const rows = lineItemUtils.readRows({ + value: p.lineItems, + arrayKey: 'lineItems', + displayName: LINE_ITEMS_DISPLAY_NAME, + }); - const lineItems = rawItems.map((item, index) => - buildOrderLineItem({ item, index, orderCurrency: p.currency }) + const lineItems = rows.map((row, index) => + buildOrderLineItem({ row, index, orderCurrency: p.currency }) ); const lineItemResources = lineItems.map((li) => li.resource); const lineItemsRelData = lineItems.map((li) => li.ref); @@ -484,62 +486,155 @@ function buildAddressResource({ }); } -function toOrderLineItemRow(item: Record): OrderLineItemRow { - const row: OrderLineItemRow = {}; - for (const field of LINE_ITEM_TEXT_FIELDS) { - const value = item[field]; - if (typeof value === 'string') row[field] = value; - } - for (const field of LINE_ITEM_NUMBER_FIELDS) { - const value = item[field]; - if (typeof value === 'number') row[field] = value; - else if (typeof value === 'string') row[field] = Number(value); - } - return row; +function orderText({ row, index, field, label }: OrderFieldParams): string | undefined { + return lineItemUtils.optionalString({ + row, + index, + field, + label, + displayName: LINE_ITEMS_DISPLAY_NAME, + }); +} + +function orderRequiredText({ row, index, field, label }: OrderFieldParams): string { + return lineItemUtils.requiredString({ + row, + index, + field, + label, + displayName: LINE_ITEMS_DISPLAY_NAME, + }); +} + +function orderNumber({ + row, + index, + field, + label, + min, +}: OrderFieldParams & { min?: number }): number | undefined { + return lineItemUtils.optionalNumber({ + row, + index, + field, + label, + displayName: LINE_ITEMS_DISPLAY_NAME, + min, + }); +} + +function orderRequiredNumber({ + row, + index, + field, + label, + min, +}: OrderFieldParams & { min?: number }): number { + return lineItemUtils.requiredNumber({ + row, + index, + field, + label, + displayName: LINE_ITEMS_DISPLAY_NAME, + min, + }); } function buildOrderLineItem({ - item, + row, index, orderCurrency, }: { - item: Record; + row: Record; index: number; orderCurrency: string | null | undefined; }): { resource: Record; ref: { type: string; id: string } } { const lid = `li_${index + 1}`; - const row = toOrderLineItemRow(item); const attributes: Record = { fromExternalSource: true, - productSku: row.productSku ?? '', - quantity: Number(row.quantity), - value: Number(row.value), - currency: row.currency || orderCurrency || 'USD', - priceType: row.priceType ?? 10, + productSku: orderRequiredText({ + row, + index, + field: 'productSku', + label: 'Product SKU', + }), + quantity: orderRequiredNumber({ + row, + index, + field: 'quantity', + label: 'Quantity', + min: 0, + }), + value: orderRequiredNumber({ + row, + index, + field: 'value', + label: 'Unit Price', + min: 0, + }), + currency: + orderText({ row, index, field: 'currency', label: 'Currency' }) || + orderCurrency || + 'USD', + priceType: + orderNumber({ row, index, field: 'priceType', label: 'Price Type' }) ?? 10, ...jsonApiBodyUtils.pickDefined({ - productName: row.productName, - freeFormProduct: row.freeFormProduct, - comment: row.comment, - shipBy: row.shipBy, + productName: orderText({ + row, + index, + field: 'productName', + label: 'Product Name', + }), + freeFormProduct: orderText({ + row, + index, + field: 'freeFormProduct', + label: 'Free-Form Product', + }), + comment: orderText({ row, index, field: 'comment', label: 'Comment' }), + shipBy: orderText({ row, index, field: 'shipBy', label: 'Ship By Date' }), + shippingEstimateAmount: orderNumber({ + row, + index, + field: 'shippingEstimateAmount', + label: 'Shipping Estimate Amount', + }), + shippingMethod: orderText({ + row, + index, + field: 'shippingMethod', + label: 'Shipping Method', + }), + shippingMethodType: orderText({ + row, + index, + field: 'shippingMethodType', + label: 'Shipping Method Type', + }), }), - - ...(row.shippingEstimateAmount != null - ? { shippingEstimateAmount: row.shippingEstimateAmount } - : {}), - - ...(row.shippingMethod ? { shippingMethod: row.shippingMethod } : {}), - - ...(row.shippingMethodType - ? { shippingMethodType: row.shippingMethodType } - : {}), }; const relationships: Record = jsonApiBodyUtils.buildRels({ - productUnit: ['productunits', row.productUnit], - product: ['products', row.productId?.trim()], - warehouse: ['warehouses', row.warehouseId?.trim() || row.warehouse?.trim()], + productUnit: [ + 'productunits', + orderRequiredText({ + row, + index, + field: 'productUnit', + label: 'Product Unit', + }), + ], + product: [ + 'products', + orderText({ row, index, field: 'productId', label: 'Product: Raw ID' }), + ], + warehouse: [ + 'warehouses', + orderText({ row, index, field: 'warehouseId', label: 'Warehouse' }) ?? + orderText({ row, index, field: 'warehouse', label: 'Warehouse' }), + ], }); return { @@ -548,27 +643,11 @@ function buildOrderLineItem({ }; } -const LINE_ITEM_TEXT_FIELDS = [ - 'productId', - 'productName', - 'freeFormProduct', - 'productSku', - 'productUnit', - 'currency', - 'comment', - 'shipBy', - 'shippingMethod', - 'shippingMethodType', - 'warehouse', - 'warehouseId', -] as const; - -const LINE_ITEM_NUMBER_FIELDS = [ - 'quantity', - 'value', - 'priceType', - 'shippingEstimateAmount', -] as const; +const LINE_ITEMS_DISPLAY_NAME = 'Line Items'; -type OrderLineItemRow = Partial> & - Partial>; +type OrderFieldParams = { + row: Record; + index: number; + field: string; + label: string; +}; diff --git a/packages/pieces/community/orocommerce/src/lib/common/index.ts b/packages/pieces/community/orocommerce/src/lib/common/index.ts index b48c0715d530..9698dfa11c3d 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/index.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -2,4 +2,5 @@ export * from './auth'; export * from './client'; export * from './props'; export * from './address-props'; +export * from './line-items'; export * from './jsonapi'; diff --git a/packages/pieces/community/orocommerce/src/lib/common/line-items.ts b/packages/pieces/community/orocommerce/src/lib/common/line-items.ts new file mode 100644 index 000000000000..ad2205ef5e1f --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/line-items.ts @@ -0,0 +1,238 @@ +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isBlank(value: unknown): boolean { + return ( + value === undefined || + value === null || + (typeof value === 'string' && value.trim() === '') + ); +} + +function at({ + displayName, + index, + label, +}: { + displayName: string; + index: number; + label: string; +}): string { + return `${displayName} row ${index + 1}, "${label}"`; +} + +function describeValue(value: unknown): string { + if (value === undefined) return 'no value'; + if (value === null) return 'null'; + if (typeof value === 'string') return `"${value}"`; + if (typeof value === 'object') { + return Array.isArray(value) ? 'an array' : 'an object'; + } + return String(value); +} + +function toMinorUnits(value: number): number { + return Math.round(value * 100); +} + +function parseNumber(value: unknown): number | undefined { + if (typeof value === 'number') { + return Number.isFinite(value) ? value : undefined; + } + if (typeof value === 'string') { + const trimmed = value.trim(); + if (trimmed === '') return undefined; + const parsed = Number(trimmed); + return Number.isFinite(parsed) ? parsed : undefined; + } + return undefined; +} + +function readRows({ + value, + arrayKey, + displayName, +}: ReadRowsParams): LineItemRow[] { + const container = isRecord(value) ? value : undefined; + const raw = + container?.[arrayKey] ?? (Array.isArray(value) ? value : undefined); + + if (!Array.isArray(raw) || raw.length === 0) { + throw new Error( + `${displayName}: add at least one row before running this step.` + ); + } + + return raw.map((row, index) => { + if (!isRecord(row)) { + throw new Error( + `${displayName} row ${index + 1}: expected a set of fields, got ${describeValue(row)}.` + ); + } + return row; + }); +} + +function requiredNumber({ + row, + index, + field, + label, + displayName, + integer = false, + min, +}: NumberFieldParams): number { + const parsed = parseNumber(row[field]); + + if (parsed === undefined) { + throw new Error( + `${at({ displayName, index, label })} must be a number, got ${describeValue(row[field])}.` + ); + } + if (integer && !Number.isInteger(parsed)) { + throw new Error( + `${at({ displayName, index, label })} must be a whole number, got ${parsed}.` + ); + } + if (min !== undefined && parsed < min) { + throw new Error( + `${at({ displayName, index, label })} must be ${min} or greater, got ${parsed}.` + ); + } + + return parsed; +} + +function optionalNumber({ + row, + index, + field, + label, + displayName, + integer = false, + min, +}: NumberFieldParams): number | undefined { + if (isBlank(row[field])) { + return undefined; + } + return requiredNumber({ row, index, field, label, displayName, integer, min }); +} + +function requiredString({ + row, + index, + field, + label, + displayName, + maxLength, +}: StringFieldParams): string { + const value = row[field]; + + if (isBlank(value)) { + throw new Error( + `${at({ displayName, index, label })} is required, got ${describeValue(value)}.` + ); + } + if (typeof value !== 'string') { + throw new Error( + `${at({ displayName, index, label })} must be text, got ${describeValue(value)}.` + ); + } + const trimmed = value.trim(); + if (maxLength !== undefined && trimmed.length > maxLength) { + throw new Error( + `${at({ displayName, index, label })} must be ${maxLength} characters or fewer, got ${trimmed.length}.` + ); + } + + return trimmed; +} + +function optionalString({ + row, + index, + field, + label, + displayName, + maxLength, +}: StringFieldParams): string | undefined { + if (isBlank(row[field])) { + return undefined; + } + return requiredString({ row, index, field, label, displayName, maxLength }); +} + +function assertSumMatches({ + rows, + field, + label, + displayName, + total, + totalLabel, + toleranceMinorUnits = 0, +}: SumParams): void { + const expected = parseNumber(total); + if (expected === undefined) { + throw new Error( + `"${totalLabel}" must be a number, got ${describeValue(total)}.` + ); + } + + const sum = rows.reduce( + (acc, row, index) => + acc + requiredNumber({ row, index, field, label, displayName }), + 0 + ); + + if (Math.abs(toMinorUnits(sum) - toMinorUnits(expected)) > toleranceMinorUnits) { + throw new Error( + `"${totalLabel}" is ${expected}, but the sum of "${label}" across ${rows.length} row(s) is ${sum}. ` + + `Correct the total or the row amounts.` + ); + } +} + +export const lineItemUtils = { + readRows, + requiredNumber, + optionalNumber, + requiredString, + optionalString, + assertSumMatches, +}; + +export type LineItemRow = Record; + +type ReadRowsParams = { + value: unknown; + arrayKey: string; + displayName: string; +}; + +type FieldParams = { + row: LineItemRow; + index: number; + field: string; + label: string; + displayName: string; +}; + +type NumberFieldParams = FieldParams & { + integer?: boolean; + min?: number; +}; + +type StringFieldParams = FieldParams & { + maxLength?: number; +}; + +type SumParams = { + rows: LineItemRow[]; + field: string; + label: string; + displayName: string; + total: unknown; + totalLabel: string; + toleranceMinorUnits?: number; +}; diff --git a/packages/pieces/community/orocommerce/test/line-items.test.ts b/packages/pieces/community/orocommerce/test/line-items.test.ts new file mode 100644 index 000000000000..0367928b7d99 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/line-items.test.ts @@ -0,0 +1,242 @@ +import { describe, expect, it } from 'vitest'; +import { lineItemUtils } from '../src/lib/common/line-items'; + +const displayName = 'Line Items'; + +describe('lineItemUtils.readRows', () => { + it('unwraps the DynamicProperties container', () => { + const rows = lineItemUtils.readRows({ + value: { lineItems: [{ quantity: 1 }] }, + arrayKey: 'lineItems', + displayName, + }); + expect(rows).toEqual([{ quantity: 1 }]); + }); + + it('accepts a plain array', () => { + const rows = lineItemUtils.readRows({ + value: [{ quantity: 2 }], + arrayKey: 'lineItems', + displayName, + }); + expect(rows).toEqual([{ quantity: 2 }]); + }); + + it('rejects an empty or missing collection', () => { + expect(() => + lineItemUtils.readRows({ value: {}, arrayKey: 'lineItems', displayName }) + ).toThrow(/add at least one row/); + expect(() => + lineItemUtils.readRows({ + value: { lineItems: [] }, + arrayKey: 'lineItems', + displayName, + }) + ).toThrow(/add at least one row/); + expect(() => + lineItemUtils.readRows({ + value: undefined, + arrayKey: 'lineItems', + displayName, + }) + ).toThrow(/add at least one row/); + }); + + it('rejects non-object rows and reports a 1-based index', () => { + expect(() => + lineItemUtils.readRows({ + value: { lineItems: [{ quantity: 1 }, 'oops'] }, + arrayKey: 'lineItems', + displayName, + }) + ).toThrow(/row 2: expected a set of fields, got "oops"/); + }); +}); + +describe('lineItemUtils.requiredNumber', () => { + const base = { index: 0, field: 'quantity', label: 'Quantity', displayName }; + + it('accepts numbers and numeric strings', () => { + expect(lineItemUtils.requiredNumber({ ...base, row: { quantity: 3 } })).toBe(3); + expect(lineItemUtils.requiredNumber({ ...base, row: { quantity: ' 2.5 ' } })).toBe(2.5); + }); + + it('rejects missing values instead of producing NaN', () => { + expect(() => lineItemUtils.requiredNumber({ ...base, row: {} })).toThrow( + /"Quantity" must be a number, got no value/ + ); + }); + + it('rejects a blank string instead of producing 0', () => { + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: ' ' } }) + ).toThrow(/must be a number/); + }); + + it('rejects non-numeric strings, NaN and Infinity', () => { + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: 'abc' } }) + ).toThrow(/must be a number, got "abc"/); + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: Number.NaN } }) + ).toThrow(/must be a number/); + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: Number.POSITIVE_INFINITY } }) + ).toThrow(/must be a number/); + }); + + it('rejects a boolean that Number() would silently turn into 1', () => { + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: true } }) + ).toThrow(/must be a number, got true/); + }); + + it('enforces integer and min constraints', () => { + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: 1.5 }, integer: true }) + ).toThrow(/must be a whole number/); + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: -1 }, min: 0 }) + ).toThrow(/must be 0 or greater/); + expect( + lineItemUtils.requiredNumber({ ...base, row: { quantity: 0 }, min: 0 }) + ).toBe(0); + }); +}); + +describe('lineItemUtils.optionalNumber', () => { + const base = { index: 1, field: 'note', label: 'Note', displayName }; + + it('returns undefined for empty input', () => { + expect(lineItemUtils.optionalNumber({ ...base, row: {} })).toBeUndefined(); + expect(lineItemUtils.optionalNumber({ ...base, row: { note: '' } })).toBeUndefined(); + expect(lineItemUtils.optionalNumber({ ...base, row: { note: null } })).toBeUndefined(); + }); + + it('treats a whitespace-only value as absent rather than failing the run', () => { + expect(lineItemUtils.optionalNumber({ ...base, row: { note: ' ' } })).toBeUndefined(); + }); + + it('still validates a provided value', () => { + expect(() => + lineItemUtils.optionalNumber({ ...base, row: { note: 'x' } }) + ).toThrow(/row 2, "Note" must be a number/); + }); +}); + +describe('lineItemUtils.requiredString', () => { + const base = { index: 0, field: 'description', label: 'Description', displayName }; + + it('rejects blank values', () => { + expect(() => + lineItemUtils.requiredString({ ...base, row: { description: ' ' } }) + ).toThrow(/is required/); + expect(() => + lineItemUtils.requiredString({ ...base, row: {} }) + ).toThrow(/is required, got no value/); + }); + + it('reports a wrong type as a type problem, not as a missing value', () => { + expect(() => + lineItemUtils.requiredString({ ...base, row: { description: 42 } }) + ).toThrow(/must be text, got 42/); + }); + + it('trims surrounding whitespace so ids stay usable as relationship targets', () => { + expect( + lineItemUtils.requiredString({ + ...base, + row: { description: ' Widget ' }, + }) + ).toBe('Widget'); + }); + + it('enforces maxLength', () => { + expect(() => + lineItemUtils.requiredString({ + ...base, + row: { description: 'abcdef' }, + maxLength: 3, + }) + ).toThrow(/3 characters or fewer, got 6/); + }); +}); + +describe('lineItemUtils.optionalString', () => { + const base = { index: 0, field: 'note', label: 'Note', displayName }; + + it('returns undefined for empty input', () => { + expect(lineItemUtils.optionalString({ ...base, row: {} })).toBeUndefined(); + expect(lineItemUtils.optionalString({ ...base, row: { note: '' } })).toBeUndefined(); + }); + + it('treats a whitespace-only value as absent rather than failing the run', () => { + expect(lineItemUtils.optionalString({ ...base, row: { note: ' ' } })).toBeUndefined(); + }); +}); + +describe('lineItemUtils.assertSumMatches', () => { + const params = { + field: 'rowTotal', + label: 'Row Total', + displayName, + totalLabel: 'Total Amount', + }; + + it('passes when the rows add up', () => { + expect(() => + lineItemUtils.assertSumMatches({ + ...params, + rows: [{ rowTotal: 10.1 }, { rowTotal: 20.2 }], + total: 30.3, + }) + ).not.toThrow(); + }); + + it('is immune to binary floating point drift', () => { + expect(() => + lineItemUtils.assertSumMatches({ + ...params, + rows: [{ rowTotal: 0.1 }, { rowTotal: 0.2 }], + total: 0.3, + }) + ).not.toThrow(); + }); + + it('reports the mismatch with both figures', () => { + expect(() => + lineItemUtils.assertSumMatches({ + ...params, + rows: [{ rowTotal: 10 }, { rowTotal: 5 }], + total: 20, + }) + ).toThrow(/"Total Amount" is 20, but the sum of "Row Total" across 2 row\(s\) is 15/); + }); + + it('honours an explicit rounding tolerance', () => { + expect(() => + lineItemUtils.assertSumMatches({ + ...params, + rows: [{ rowTotal: 10 }, { rowTotal: 5 }], + total: 15.01, + toleranceMinorUnits: 1, + }) + ).not.toThrow(); + }); + + it('rejects a non-numeric total', () => { + expect(() => + lineItemUtils.assertSumMatches({ ...params, rows: [{ rowTotal: 1 }], total: 'x' }) + ).toThrow(/"Total Amount" must be a number, got "x"/); + }); + + it('surfaces an invalid row amount rather than summing it as NaN', () => { + expect(() => + lineItemUtils.assertSumMatches({ + ...params, + rows: [{ rowTotal: 1 }, {}], + total: 1, + }) + ).toThrow(/row 2, "Row Total" must be a number/); + }); +}); From 0368b38e9902ea575ff2bbc435ea59ad964db3f2 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 13 Aug 2026 16:49:08 +0200 Subject: [PATCH 32/50] fix(orocommerce): reject an update that would change nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update-customer, update-user and update-customer-user assembled attributes and relationships from whatever the user filled in. Filling in nothing still sent {data:{type,id,attributes:{},relationships:{}}}; sanitizeJsonApiBody stripped the empty containers, Oro changed nothing, and the step reported success. A flow built on that reads as working while doing nothing at all. The three actions now refuse the call when both maps come back empty, naming themselves so the message is useful in a flow with several update steps. The accompanying tests also cover the invoice line-item validation added in the previous commit, because both are failures that must surface before any request reaches Oro — the tests would hit the network if either guard let them through. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/actions/update-customer-user.ts | 6 ++ .../src/lib/actions/update-customer.ts | 6 ++ .../src/lib/actions/update-user.ts | 6 ++ .../src/lib/common/jsonapi/body-utils.ts | 21 +++- .../orocommerce/test/action-guards.test.ts | 102 ++++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 packages/pieces/community/orocommerce/test/action-guards.test.ts diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index 42a83874c635..bd32cb926083 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -131,6 +131,12 @@ export const updateCustomerUserAction = createAction({ ...extraRels, }; + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes, + relationships, + actionName: 'Update Customer User', + }); + const response = await oroApiCall({ method: HttpMethod.PATCH, resourceUri: `/customerusers/${p.customerUserId}`, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts index 8b9214a68ef9..4f4115f7f582 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts @@ -94,6 +94,12 @@ export const updateCustomerAction = createAction({ ...extraRels, }; + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes, + relationships, + actionName: 'Update Customer', + }); + const response = await oroApiCall({ method: HttpMethod.PATCH, resourceUri: `/customers/${p.customerId}`, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts index 7936f0f3a36c..86cc3c112fd3 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -141,6 +141,12 @@ export const updateUserAction = createAction({ ...extraRels, }; + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes, + relationships, + actionName: 'Update User', + }); + const response = await oroApiCall({ method: HttpMethod.PATCH, resourceUri: `/users/${p.userId}`, diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts index 71caf6fa0580..0d7f4d864868 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts @@ -81,4 +81,23 @@ function parseAdditionalRelations( return obj; } -export const jsonApiBodyUtils = { pickDefined, omitEmptyObjects, buildRels, parseAdditionalAttributes, parseAdditionalRelations }; +function assertUpdateNotEmpty({ + attributes, + relationships, + actionName, +}: { + attributes: Record; + relationships: Record; + actionName: string; +}): void { + if ( + Object.keys(attributes).length === 0 && + Object.keys(relationships).length === 0 + ) { + throw new Error( + `${actionName}: nothing to update. Fill in at least one field or relationship.` + ); + } +} + +export const jsonApiBodyUtils = { pickDefined, omitEmptyObjects, buildRels, parseAdditionalAttributes, parseAdditionalRelations, assertUpdateNotEmpty }; diff --git a/packages/pieces/community/orocommerce/test/action-guards.test.ts b/packages/pieces/community/orocommerce/test/action-guards.test.ts new file mode 100644 index 000000000000..cb407a08c65e --- /dev/null +++ b/packages/pieces/community/orocommerce/test/action-guards.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest'; +import { createMockActionContext } from '@activepieces/pieces-framework'; +import { createInvoiceAction } from '../src/lib/actions/create-invoice'; +import { updateCustomerAction } from '../src/lib/actions/update-customer'; +import { updateUserAction } from '../src/lib/actions/update-user'; +import { updateCustomerUserAction } from '../src/lib/actions/update-customer-user'; + +function invoiceProps(lineItems: Record[]) { + return { + invoiceDate: '2026-01-01', + currency: 'USD', + customerName: 'Acme Inc.', + totalAmount: 100, + lineItems: { lineItems }, + }; +} + +function runInvoice(lineItems: Record[]) { + return createInvoiceAction.run( + createMockActionContext({ + propsValue: invoiceProps(lineItems), + }) + ); +} + +describe('create invoice rejects unusable line items before calling Oro', () => { + it('reports a missing quantity instead of sending null', async () => { + await expect( + runInvoice([ + { description: 'Widget', unitPrice: 10, rowTotal: 100 }, + ]) + ).rejects.toThrow('Line Items row 1, "Quantity" must be a number, got no value.'); + }); + + it('reports a non-numeric quantity with the offending value', async () => { + await expect( + runInvoice([ + { description: 'Widget', quantity: 'ten', unitPrice: 10, rowTotal: 100 }, + ]) + ).rejects.toThrow('Line Items row 1, "Quantity" must be a number, got "ten".'); + }); + + it('reports a missing description', async () => { + await expect( + runInvoice([{ quantity: 10, unitPrice: 10, rowTotal: 100 }]) + ).rejects.toThrow('Line Items row 1, "Description" is required, got no value.'); + }); + + it('reports a total that does not match the row totals', async () => { + await expect( + runInvoice([ + { description: 'Widget', quantity: 1, unitPrice: 10, rowTotal: 10 }, + ]) + ).rejects.toThrow( + '"Total Amount" is 100, but the sum of "Row Total" across 1 row(s) is 10.' + ); + }); + + it('rejects an empty line item list', async () => { + await expect(runInvoice([])).rejects.toThrow( + 'Line Items: add at least one row before running this step.' + ); + }); +}); + +describe('update actions refuse to send an empty request', () => { + it('Update Customer', async () => { + await expect( + updateCustomerAction.run( + createMockActionContext({ + propsValue: { customerId: '1' }, + }) + ) + ).rejects.toThrow( + 'Update Customer: nothing to update. Fill in at least one field or relationship.' + ); + }); + + it('Update User', async () => { + await expect( + updateUserAction.run( + createMockActionContext({ + propsValue: { userId: '1' }, + }) + ) + ).rejects.toThrow( + 'Update User: nothing to update. Fill in at least one field or relationship.' + ); + }); + + it('Update Customer User', async () => { + await expect( + updateCustomerUserAction.run( + createMockActionContext({ + propsValue: { customerUserId: '1' }, + }) + ) + ).rejects.toThrow( + 'Update Customer User: nothing to update. Fill in at least one field or relationship.' + ); + }); +}); From 9d0aa5e637467b64a86a23ebc06aa4c90ed26000 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 13 Aug 2026 16:49:09 +0200 Subject: [PATCH 33/50] fix(orocommerce): stop carrying an invoice PDF as base64 text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit invoicePdfContent was a LongText holding a base64 PDF. A multi-megabyte string in a text prop travels in the request body, sits in worker memory, and lands in the run log; it also collides with AP_MAX_FILE_SIZE_MB and AP_MAX_FLOW_RUN_LOG_SIZE_MB. It is now a File prop, so the runtime handles the transfer and the action reads .base64 at the point it builds the JSON:API resource. The filename default moves to the uploaded file's own name, which also removes the duplicated 'invoice.pdf' literal that was set in two places. refCustomerId and refCustomerUserId were Number props whose descriptions call them arbitrary external identifiers. External ids are routinely strings, and leading zeros do not survive a Number — both are now ShortText. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lib/actions/create-invoice.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index 33a7eb553ab7..1efc014cd853 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -41,13 +41,13 @@ export const createInvoiceAction = createAction({ }), customer: customerDropdown, customerUser: customerUserDropdown(false), - refCustomerId: Property.Number({ + refCustomerId: Property.ShortText({ displayName: 'External Customer ID', description: 'An optional ID reference to a customer. Can be used for storing an arbitrary external ID.', required: false, }), - refCustomerUserId: Property.Number({ + refCustomerUserId: Property.ShortText({ displayName: 'External Customer User ID', description: 'An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.', @@ -107,17 +107,17 @@ export const createInvoiceAction = createAction({ description: 'URL for the external payment page.', required: false, }), - invoicePdfContent: Property.LongText({ - displayName: 'Invoice PDF (Base64)', + invoicePdfContent: Property.File({ + displayName: 'Invoice PDF', description: - 'Base64-encoded PDF file content. When provided, the file is attached to the invoice as the default PDF.', + 'PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.', required: false, }), invoicePdfFilename: Property.ShortText({ displayName: 'Invoice PDF Filename', - description: 'Filename for the attached PDF (e.g. invoice.pdf).', + description: + 'Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.', required: false, - defaultValue: 'invoice.pdf', }), organization: organizationDropdown, @@ -279,8 +279,9 @@ export const createInvoiceAction = createAction({ id: 'invoiceDefaultPdfFile', attributes: { mimeType: 'application/pdf', - originalFilename: p.invoicePdfFilename || 'invoice.pdf', - content: p.invoicePdfContent, + originalFilename: + p.invoicePdfFilename || p.invoicePdfContent.filename, + content: p.invoicePdfContent.base64, }, } : undefined; From 7ca5361063ff15d4867b81c6cba4f24645f378e0 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 13 Aug 2026 16:49:31 +0200 Subject: [PATCH 34/50] feat(orocommerce): gate the translation files in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regenerating the piece's translation files was a manual step, so it stopped happening. The locale files carried 175 keys against the 255 the piece actually exposes: 46 belonged to props that no longer exist, and 126 current strings were missing entirely. The translations themselves are good and are left untouched. tools/check-i18n.mjs closes the loop. It imports the built piece, walks the same 19 metadata paths as pieceTranslation.pathsToValuesToTranslate and truncates keys at the same 512 characters, so it derives exactly the key set the official generator does — verified by diffing its output against "cli pieces generate-translation-file orocommerce", which matches key for key and value for value. It fails on keys missing from or stale in translation.json, on any locale file whose key set differs from it, and on empty values, and it names the offending keys rather than only counting them. A value identical to the English source is a warning, since GET and Website are the same word in several of these languages; --strict-untranslated promotes it. i18n:write reconciles without the monorepo CLI: stale keys go, missing keys are seeded with the English text, existing translations are kept. --bundle= and --i18n-dir= override the paths for use outside this layout. The check runs in CI alongside the piece's tests, which were not running at all before — ci.yml's test step is a hardcoded filter list that did not name this piece, so both jsonapi-roundtrip and the new line-item tests were local-only. Two warnings are expected and left standing: pl.json and uk.json are never loaded by the runtime, because LocalesEnum has no Polish or Ukrainian. They stay in sync so they are ready if those locales are ever added. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 5 +- .../pieces/community/orocommerce/package.json | 4 +- .../community/orocommerce/src/i18n/de.json | 173 ++++++++---- .../community/orocommerce/src/i18n/fr.json | 172 ++++++++---- .../community/orocommerce/src/i18n/nl.json | 173 ++++++++---- .../community/orocommerce/src/i18n/pl.json | 172 ++++++++---- .../orocommerce/src/i18n/translation.json | 172 ++++++++---- .../community/orocommerce/src/i18n/uk.json | 172 ++++++++---- .../orocommerce/tools/check-i18n.mjs | 259 ++++++++++++++++++ turbo.json | 5 + 10 files changed, 1027 insertions(+), 280 deletions(-) create mode 100644 packages/pieces/community/orocommerce/tools/check-i18n.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8f3442b1e23..b59a0731922c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,10 @@ jobs: set -euo pipefail pids=() - npx turbo run test --filter=@activepieces/engine --filter=@activepieces/shared --filter=@activepieces/sandbox --filter=@activepieces/ai-providers --filter=@activepieces/pieces-framework & + npx turbo run test --filter=@activepieces/engine --filter=@activepieces/shared --filter=@activepieces/sandbox --filter=@activepieces/ai-providers --filter=@activepieces/pieces-framework --filter=@activepieces/piece-orocommerce & + pids+=($!) + + npx turbo run i18n:check --filter=@activepieces/piece-orocommerce & pids+=($!) npx turbo run test-ce test-ee test-cloud check-migrations --filter=api & diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index 1a4e03362845..78d062a647e1 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -13,7 +13,9 @@ "build": "tsc -p tsconfig.lib.json && cp package.json dist/", "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'", "bundle": "node ../../../../dist/packages/cli/src/index.js pieces bundle", - "test": "vitest run" + "test": "vitest run", + "i18n:check": "node tools/check-i18n.mjs", + "i18n:write": "node tools/check-i18n.mjs --write" }, "devDependencies": { "vitest": "3.2.6", diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json index c1c5c9044958..2f8b1e464a13 100644 --- a/packages/pieces/community/orocommerce/src/i18n/de.json +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -4,21 +4,37 @@ "Admin Prefix": "Admin-Präfix", "Client ID": "Client-ID", "Client Secret": "Client-Secret", + "Default HTTP Headers": "Default HTTP Headers", + "Internal infrastructure": "Internal infrastructure", "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "Die Basis-URL Ihrer OroCommerce-Instanz (z. B. https://your-store.com).", "The admin panel URL prefix (default is \"admin\").": "Das URL-Präfix des Adminbereichs (Standard ist \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "Die OAuth-Client-ID aus Ihrer OroCommerce-OAuth-Anwendung.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Das OAuth-Client-Secret aus Ihrer OroCommerce-OAuth-Anwendung.", + "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthentifizieren Sie sich bei den OroCommerce-APIs mit OAuth 2.0 Client Credentials.\n\n**Schritte zum Abrufen der Zugangsdaten:**\n1. Melden Sie sich in Ihrem OroCommerce-Administrationsbereich an.\n2. Navigieren Sie zu **System** > **User Management** > **OAuth Applications**.\n3. Klicken Sie auf **Create OAuth Application** und konfigurieren Sie:\n - **Application Name**: Geben Sie einen aussagekräftigen Namen ein (z. B. \"Activepieces Integration\")\n - **Grants**: Wählen Sie **Client Credentials**\n - **Redirect URIs**: Für den Client-Credentials-Flow nicht erforderlich\n4. Speichern Sie die Anwendung und kopieren Sie die **Client ID** und das **Client Secret**.\n5. Notieren Sie Ihre **Server URL** (z. B. `https://your-store.com`) und das **Admin Prefix** (in der Regel `admin`).\n ", "Create Invoice": "Rechnung erstellen", "Create Order": "Bestellung erstellen", - "API Call": "API-Aufruf", + "Create Customer": "Create Customer", + "Update Customer": "Update Customer", + "Create Customer User": "Create Customer User", + "Update Customer User": "Update Customer User", + "Create User": "Create User", + "Update User": "Update User", + "Custom API Call": "Custom API Call", "Serialize JSON:API Request": "JSON:API-Anfrage serialisieren", "Unserialize JSON:API Response": "JSON:API-Antwort deserialisieren", "Creates a new invoice record in OroCommerce.": "Erstellt einen neuen Rechnungsdatensatz in OroCommerce.", "Creates a new order record in OroCommerce.": "Erstellt einen neuen Bestelldatensatz in OroCommerce.", + "Creates a new customer (company) record in OroCommerce.": "Creates a new customer (company) record in OroCommerce.", + "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.": "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.", + "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", + "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", + "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Führt einen direkten authentifizierten Aufruf der OroCommerce JSON:API aus.", - "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Wandelt ein einfaches Objekt in einen JSON:API-konformen Anfrage-Body um. Beziehungsfelder werden automatisch erkannt – jede Array- oder Objektvariable wird als Beziehung behandelt, nur Skalare werden zu Attributen. Das Ergebnis kann direkt als Anfrage-Body der API-Aufruf-Aktion verwendet werden.", - "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Macht einen JSON:API-Antwort-Body flach (einfaches Objekt oder Array von Objekten). Eingeschlossene Beziehungen werden aufgelöst und inline eingefügt. Nicht aufgelöste Beziehungsreferenzen werden als { _type, id }-Stubs beibehalten, sodass das Dokument ohne Datenverlust wieder in eine gültige JSON:API-Anfrage serialisiert werden kann. Übergeben Sie die Ausgabe \"body\" der API-Aufruf-Aktion als Response-Eingabe.", + "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", + "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", "Invoice Date": "Rechnungsdatum", "Currency": "Währung", "Customer Name": "Kundenname", @@ -36,11 +52,16 @@ "Shipping Method": "Versandmethode", "Seller Info": "Verkäuferinformationen", "External Payment URL": "Externe Zahlungs-URL", + "Invoice PDF": "Invoice PDF", + "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organisation", - "User": "Benutzer", + "Owner": "Owner", "Website": "Website", "Internal Status": "Interner Status", "Line Items": "Positionen", + "Additional Attributes": "Additional Attributes", + "Additional Relations": "Additional Relations", + "Additional Headers": "Additional Headers", "Identifier": "Bezeichner", "PO Number": "Bestellnummer (PO)", "Customer Notes": "Kundennotizen", @@ -78,11 +99,46 @@ "Shipping: Region / State": "Versand: Region / Bundesland", "Shipping: Custom Region": "Versand: Benutzerdefinierte Region", "Product": "Produkt", + "Name": "Name", + "External ID": "External ID", + "VAT ID": "VAT ID", + "Parent Customer": "Parent Customer", + "Customer Group": "Customer Group", + "Tax Code": "Tax Code", + "Internal Rating": "Internal Rating", + "Addresses": "Addresses", + "Customer ID": "Customer ID", + "Email": "Email", + "Password": "Password", + "Name Prefix": "Name Prefix", + "First Name": "First Name", + "Middle Name": "Middle Name", + "Last Name": "Last Name", + "Name Suffix": "Name Suffix", + "Enabled": "Enabled", + "Confirmed": "Confirmed", + "Birthday": "Birthday", + "Roles (replaces all existing roles)": "Roles (replaces all existing roles)", + "Customer User ID": "Customer User ID", + "Username": "Username", + "Phone": "Phone", + "Owner (Business Unit)": "Owner (Business Unit)", + "Business Units (replaces all existing business units)": "Business Units (replaces all existing business units)", + "User Roles (replaces all existing roles)": "User Roles (replaces all existing roles)", + "Organizations (replaces all existing organizations)": "Organizations (replaces all existing organizations)", + "User Groups (replaces all existing groups)": "User Groups (replaces all existing groups)", + "Auth Status": "Auth Status", + "User ID": "User ID", + "Business Unit": "Business Unit", "Method": "Methode", - "Resource URI": "Ressourcen-URI", "Headers": "Header", "Query Parameters": "Abfrageparameter", - "Request Body": "Anfrage-Body", + "Body Type": "Body Type", + "Body": "Body", + "Response is Binary ?": "Response is Binary ?", + "No Error on Failure": "No Error on Failure", + "Timeout (in seconds)": "Timeout (in seconds)", + "Follow redirects": "Follow redirects", "Resource Type": "Ressourcentyp", "Resource ID": "Ressourcen-ID", "Attributes": "Attribute", @@ -93,9 +149,7 @@ "ISO-4217 3-letter currency code (e.g. USD, EUR).": "ISO-4217-Währungscode mit 3 Buchstaben (z. B. USD, EUR).", "Name of the company being billed. Stored as a plain-text label on the invoice.": "Name des Unternehmens, das belastet wird. Wird als Klartextbezeichnung auf der Rechnung gespeichert.", "Select a customer.": "Wählen Sie einen Kunden aus.", - "Overrides the Customer dropdown. Paste an ID from a previous step.": "Überschreibt das Dropdown \"Kunde\". Fügen Sie eine ID aus einem vorherigen Schritt ein.", "Select a Customer User.": "Wählen Sie einen Kundenbenutzer aus.", - "Overrides the Customer User dropdown.": "Überschreibt das Dropdown \"Kundenbenutzer\".", "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Optionale ID-Referenz zu einem Kunden. Kann zum Speichern einer beliebigen externen ID verwendet werden.", "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Optionale ID-Referenz zu einem Kundenbenutzer. Kann zum Speichern einer beliebigen externen ID verwendet werden.", "Total invoice amount. Should equal the sum of all line item row totals.": "Gesamtrechnungsbetrag. Sollte der Summe aller Positionszeilen entsprechen.", @@ -108,15 +162,16 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Beschriftung der Versandmethode (z. B. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "HTML-Zeichenkette für Verkäuferkontakt/-adresse.", "URL for the external payment page.": "URL für die externe Zahlungsseite.", - "Search organizations.": "Organisationen suchen.", - "Overrides the Organization dropdown.": "Überschreibt das Dropdown \"Organisation\".", - "Search user by name, username or email.": "Benutzer nach Name, Benutzername oder E-Mail suchen.", - "Overrides the Owner dropdown.": "Überschreibt das Dropdown \"Inhaber\".", - "Search websites.": "Websites suchen.", - "Overrides the Website dropdown.": "Überschreibt das Dropdown \"Website\".", + "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", + "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "The organization this record belongs to.": "The organization this record belongs to.", + "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", + "The website this record is associated with.": "The website this record is associated with.", "Invoice internal status (e.g. Draft, Open).": "Interner Rechnungsstatus (z. B. Draft, Open).", - "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Überschreibt das Dropdown \"Interner Status\" (z. B. draft, open, paid).", "Invoice line items. Each item is sent via JSON:API included.": "Rechnungspositionen. Jedes Element wird per JSON:API included übertragen.", + "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}": "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}", + "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}": "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}", + "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}": "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}", "The customer this order belongs to.": "Der Kunde, zu dem diese Bestellung gehört.", "Unique order reference (e.g. FR1012401Z).": "Eindeutige Bestellreferenz (z. B. FR1012401Z).", "Purchase order number provided by the buyer.": "Vom Käufer angegebene Bestellnummer.", @@ -128,51 +183,75 @@ "The shipping method type (e.g. \"primary\").": "Der Typ der Versandmethode (z. B. \"primary\").", "Prevent the promotions engine from running for this order.": "Verhindern, dass die Aktions-Engine für diese Bestellung ausgeführt wird.", "Order internal status (e.g. Open, Cancelled).": "Interner Bestellstatus (z. B. Open, Cancelled).", - "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Überschreibt das Dropdown \"Interner Status\" (z. B. order_internal_status.open).", "Search payment terms.": "Zahlungsbedingungen suchen.", - "Overrides the Payment Term dropdown.": "Überschreibt das Dropdown \"Zahlungsbedingung\".", "Search warehouses.": "Lager suchen.", - "Overrides the Warehouse dropdown.": "Überschreibt das Dropdown \"Lager\".", "Search orders to use as the parent order.": "Bestellungen suchen, die als übergeordnete Bestellung verwendet werden sollen.", - "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Überschreibt das Dropdown \"Übergeordnete Bestellung\". Fügen Sie eine Bestell-ID aus einem vorherigen Schritt ein.", "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Bestellstatus, der von einem externen System verwaltet wird (nur relevant, wenn \"Enable External Status Management\" aktiviert ist).", - "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Überschreibt das Dropdown \"Status\". Wird nur angewendet, wenn \"Enable External Status Management\" aktiviert ist.", "Address label (e.g. \"Main Office\").": "Adressbezeichnung (z. B. \"Main Office\").", "ISO-3166 country. Start typing to filter the list.": "ISO-3166-Land. Beginnen Sie zu tippen, um die Liste zu filtern.", - "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Überschreibt das Dropdown \"Land (Rechnung)\" (ISO-3166-Ländercode mit 2 Buchstaben, z. B. US).", "Region or state. Select a country first. Start typing to filter.": "Region oder Bundesland. Wählen Sie zuerst ein Land aus. Beginnen Sie zu tippen, um zu filtern.", - "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Überschreibt das Dropdown \"Region (Rechnung)\" (ISO-3166-2-Code, z. B. US-NY).", "Free-text region for countries without predefined regions.": "Freitextregion für Länder ohne vordefinierte Regionen.", "Address label (e.g. \"Warehouse East\").": "Adressbezeichnung (z. B. \"Warehouse East\").", - "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Überschreibt das Dropdown \"Land (Versand)\" (ISO-3166-Ländercode mit 2 Buchstaben, z. B. US).", - "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Überschreibt das Dropdown \"Region (Versand)\" (ISO-3166-2-Code, z. B. US-NY).", "Order line items.": "Bestellpositionen.", "Search products.": "Produkte suchen.", - "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Pfad relativ zur API-Basis, z. B. /orders, /orders/42, /invoices?filter[status]=open", - "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Anfrage-Header als JSON-Objekt. Standardwerte umfassen Accept und X-Include für JSON:API.", - "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Zusätzliche Abfrageparameter als JSON-Objekt, z. B. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", - "Request body as a JSON object (used for POST / PATCH).": "Anfrage-Body als JSON-Objekt (wird für POST / PATCH verwendet).", - "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Der JSON:API-Ressourcentyp (z. B. orders, invoices, products). Das Feld _type wird automatisch als Ressourcentyp verwendet.", - "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID der Ressource – lassen Sie dieses Feld beim Erstellen eines neuen Datensatzes (POST) leer oder wenn die Eingabe ein id-Feld enthält (das id-Feld wird automatisch übernommen).", - "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Das flache Objekt, das serialisiert werden soll. Übergeben Sie hier das vollständige JSON-Dokument – Arrays und Objekte werden automatisch in \"relationships\" verschoben; nur Skalare werden zu Attributen. Für eine manuelle Erstellung verwenden Sie einfache Schlüssel/Wert-Paare, z. B. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", - "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Optionale explizite Beziehungszuordnung, die Vorrang vor allen in Attributes erkannten Beziehungen hat. Jeder Schlüssel ist ein Beziehungsname; der Wert ist ein einzelnes Verknüpfungsobjekt {\"type\":\"customers\",\"id\":\"42\"}, ein Array von Verknüpfungsobjekten oder null. Beispiel: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", - "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Optionale Liste vollständiger JSON:API-Ressourcenobjekte, die im Abschnitt \"included\" der Anfrage eingebettet werden. Wenn die Eingabe von Attributes bereits ein \"included\"-Array enthält (z. B. den Roh-Response-Body des API-Aufrufs), wird es automatisch weitergegeben – dieses Feld muss nur gesetzt werden, wenn eingeschlossene Ressourcen manuell hinzugefügt oder überschrieben werden. Jeder Eintrag muss eine gültige JSON:API-Ressource mit den Feldern \"type\", \"id\" und \"attributes\" sein.", - "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Der rohe JSON:API-Antwort-Body – das Feld \"body\" aus der Ausgabe der API-Aufruf-Aktion. Muss einen obersten Schlüssel \"data\" enthalten (einzelne Ressource oder Sammlung).", + "A human-readable name that identifies the customer (company).": "A human-readable name that identifies the customer (company).", + "A unique identifier from an external system.": "A unique identifier from an external system.", + "Customer's value added tax identification number.": "Customer's value added tax identification number.", + "The parent company this customer (division) reports to.": "The parent company this customer (division) reports to.", + "Search customer groups.": "Search customer groups.", + "Search customer tax codes.": "Search customer tax codes.", + "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").": "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").", + "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", + "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", + "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", + "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", + "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", + "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", + "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", + "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", + "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", + "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", + "Updated email address of the customer user.": "Updated email address of the customer user.", + "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Enable or disable the storefront account.": "Enable or disable the storefront account.", + "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", + "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", + "Email address of the user.": "Email address of the user.", + "Job title or position.": "Job title or position.", + "When disabled the user cannot log in. Defaults to true.": "When disabled the user cannot log in. Defaults to true.", + "The business unit that owns this record.": "The business unit that owns this record.", + "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.": "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.", + "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.": "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.", + "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.": "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.", + "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.": "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.", + "Authentication status of the user (e.g. active, reset, locked).": "Authentication status of the user (e.g. active, reset, locked).", + "The numeric ID of the user to update.": "The numeric ID of the user to update.", + "Updated login name for the user.": "Updated login name for the user.", + "Updated email address of the user.": "Updated email address of the user.", + "Enable or disable the user account.": "Enable or disable the user account.", + "Search business units.": "Search business units.", + "Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.", + "Enable for files like PDFs, images, etc.": "Enable for files like PDFs, images, etc.", + "e.g. orders, invoices, products. Taken from _type when left empty.": "e.g. orders, invoices, products. Taken from _type when left empty.", + "Leave empty to create a record, or when the input carries an id.": "Leave empty to create a record, or when the input carries an id.", + "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.": "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.", + "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", + "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", + "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", "GET": "GET", "POST": "POST", "PATCH": "PATCH", + "PUT": "PUT", "DELETE": "DELETE", - "New Order": "Neue Bestellung", - "Order Removal": "Bestellung entfernen", - "Order Update": "Bestellung aktualisieren", - "New Invoice": "Neue Rechnung", - "Invoice Removal": "Rechnung entfernen", - "Invoice Update": "Rechnung aktualisieren", - "Triggered when a new order is created": "Ausgelöst, wenn eine neue Bestellung erstellt wird", - "Triggered when an order is deleted": "Ausgelöst, wenn eine Bestellung gelöscht wird", - "Triggered when an order is updated": "Ausgelöst, wenn eine Bestellung aktualisiert wird", - "Triggered when a new invoice is created": "Ausgelöst, wenn eine neue Rechnung erstellt wird", - "Triggered when an invoice is deleted": "Ausgelöst, wenn eine Rechnung gelöscht wird", - "Triggered when an invoice is updated": "Ausgelöst, wenn eine Rechnung aktualisiert wird" + "HEAD": "HEAD", + "None": "None", + "JSON": "JSON", + "Form Data": "Form Data", + "Raw": "Raw", + "Oro Webhook Event": "Oro Webhook Event", + "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", + "Topic": "Topic", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" } - diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json index b56c9ce9e0c0..4c6ef423bd9a 100644 --- a/packages/pieces/community/orocommerce/src/i18n/fr.json +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -4,21 +4,37 @@ "Admin Prefix": "Préfixe d'administration", "Client ID": "ID client", "Client Secret": "Secret client", + "Default HTTP Headers": "Default HTTP Headers", + "Internal infrastructure": "Internal infrastructure", "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "L'URL de base de votre instance OroCommerce (par ex. https://your-store.com).", "The admin panel URL prefix (default is \"admin\").": "Le préfixe d'URL du panneau d'administration (valeur par défaut \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "L'ID client OAuth de votre application OAuth OroCommerce.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Le secret client OAuth de votre application OAuth OroCommerce.", + "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthentifiez-vous auprès des API OroCommerce en utilisant les identifiants client OAuth 2.0.\n\n**Étapes pour obtenir les identifiants :**\n1. Connectez-vous à votre panneau d'administration OroCommerce.\n2. Accédez à **System** > **User Management** > **OAuth Applications**.\n3. Cliquez sur **Create OAuth Application** et configurez :\n - **Application Name** : Saisissez un nom descriptif (par ex. \"Activepieces Integration\")\n - **Grants** : Sélectionnez **Client Credentials**\n - **Redirect URIs** : Non requis pour le flux Client Credentials\n4. Enregistrez l'application et copiez le **Client ID** et le **Client Secret**.\n5. Notez votre **Server URL** (par ex. `https://your-store.com`) et le **Admin Prefix** (généralement `admin`).\n ", "Create Invoice": "Créer une facture", "Create Order": "Créer une commande", - "API Call": "Appel d'API", + "Create Customer": "Create Customer", + "Update Customer": "Update Customer", + "Create Customer User": "Create Customer User", + "Update Customer User": "Update Customer User", + "Create User": "Create User", + "Update User": "Update User", + "Custom API Call": "Custom API Call", "Serialize JSON:API Request": "Sérialiser la requête JSON:API", "Unserialize JSON:API Response": "Désérialiser la réponse JSON:API", "Creates a new invoice record in OroCommerce.": "Crée un nouvel enregistrement de facture dans OroCommerce.", "Creates a new order record in OroCommerce.": "Crée un nouvel enregistrement de commande dans OroCommerce.", + "Creates a new customer (company) record in OroCommerce.": "Creates a new customer (company) record in OroCommerce.", + "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.": "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.", + "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", + "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", + "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Effectue un appel authentifié direct vers le JSON:API d'OroCommerce.", - "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Convertit un objet simple en corps de requête conforme à JSON:API. Les champs de relation sont détectés automatiquement : toute valeur de type tableau ou objet est traitée comme une relation, seuls les scalaires deviennent des attributs. Le résultat peut être utilisé directement comme corps de requête de l'action Appel d'API.", - "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Aplati un corps de réponse JSON:API en un objet simple (ou un tableau d'objets). Les relations incluses sont résolues et intégrées. Les références de relations non résolues sont conservées sous forme de stubs { _type, id } afin que le document puisse être à nouveau sérialisé en une requête JSON:API valide sans perte de données. Passez la sortie \"body\" de l'action Appel d'API comme entrée Response.", + "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", + "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", "Invoice Date": "Date de facture", "Currency": "Devise", "Customer Name": "Nom du client", @@ -36,11 +52,16 @@ "Shipping Method": "Méthode d'expédition", "Seller Info": "Informations vendeur", "External Payment URL": "URL de paiement externe", + "Invoice PDF": "Invoice PDF", + "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organisation", - "User": "Utilisateur", + "Owner": "Owner", "Website": "Site web", "Internal Status": "Statut interne", "Line Items": "Lignes", + "Additional Attributes": "Additional Attributes", + "Additional Relations": "Additional Relations", + "Additional Headers": "Additional Headers", "Identifier": "Identifiant", "PO Number": "Numéro de commande (PO)", "Customer Notes": "Notes client", @@ -78,11 +99,46 @@ "Shipping: Region / State": "Expédition : région / État", "Shipping: Custom Region": "Expédition : région personnalisée", "Product": "Produit", + "Name": "Name", + "External ID": "External ID", + "VAT ID": "VAT ID", + "Parent Customer": "Parent Customer", + "Customer Group": "Customer Group", + "Tax Code": "Tax Code", + "Internal Rating": "Internal Rating", + "Addresses": "Addresses", + "Customer ID": "Customer ID", + "Email": "Email", + "Password": "Password", + "Name Prefix": "Name Prefix", + "First Name": "First Name", + "Middle Name": "Middle Name", + "Last Name": "Last Name", + "Name Suffix": "Name Suffix", + "Enabled": "Enabled", + "Confirmed": "Confirmed", + "Birthday": "Birthday", + "Roles (replaces all existing roles)": "Roles (replaces all existing roles)", + "Customer User ID": "Customer User ID", + "Username": "Username", + "Phone": "Phone", + "Owner (Business Unit)": "Owner (Business Unit)", + "Business Units (replaces all existing business units)": "Business Units (replaces all existing business units)", + "User Roles (replaces all existing roles)": "User Roles (replaces all existing roles)", + "Organizations (replaces all existing organizations)": "Organizations (replaces all existing organizations)", + "User Groups (replaces all existing groups)": "User Groups (replaces all existing groups)", + "Auth Status": "Auth Status", + "User ID": "User ID", + "Business Unit": "Business Unit", "Method": "Méthode", - "Resource URI": "URI de ressource", "Headers": "En-têtes", "Query Parameters": "Paramètres de requête", - "Request Body": "Corps de la requête", + "Body Type": "Body Type", + "Body": "Body", + "Response is Binary ?": "Response is Binary ?", + "No Error on Failure": "No Error on Failure", + "Timeout (in seconds)": "Timeout (in seconds)", + "Follow redirects": "Follow redirects", "Resource Type": "Type de ressource", "Resource ID": "ID de ressource", "Attributes": "Attributs", @@ -93,9 +149,7 @@ "ISO-4217 3-letter currency code (e.g. USD, EUR).": "Code de devise ISO-4217 à 3 lettres (par ex. USD, EUR).", "Name of the company being billed. Stored as a plain-text label on the invoice.": "Nom de l'entreprise facturée. Enregistré comme libellé en texte brut sur la facture.", "Select a customer.": "Sélectionnez un client.", - "Overrides the Customer dropdown. Paste an ID from a previous step.": "Remplace la liste déroulante Client. Collez un ID provenant d'une étape précédente.", "Select a Customer User.": "Sélectionnez un utilisateur client.", - "Overrides the Customer User dropdown.": "Remplace la liste déroulante Utilisateur client.", "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Référence ID facultative vers un client. Peut être utilisée pour stocker un ID externe arbitraire.", "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Référence ID facultative vers un utilisateur client. Peut être utilisée pour stocker un ID externe arbitraire.", "Total invoice amount. Should equal the sum of all line item row totals.": "Montant total de la facture. Doit être égal à la somme de toutes les lignes.", @@ -108,15 +162,16 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Libellé de la méthode d'expédition (par ex. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "Chaîne HTML du contact/adresse du vendeur.", "URL for the external payment page.": "URL de la page de paiement externe.", - "Search organizations.": "Rechercher des organisations.", - "Overrides the Organization dropdown.": "Remplace la liste déroulante Organisation.", - "Search user by name, username or email.": "Rechercher un utilisateur par nom, nom d'utilisateur ou e-mail.", - "Overrides the Owner dropdown.": "Remplace la liste déroulante Propriétaire.", - "Search websites.": "Rechercher des sites web.", - "Overrides the Website dropdown.": "Remplace la liste déroulante Site web.", + "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", + "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "The organization this record belongs to.": "The organization this record belongs to.", + "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", + "The website this record is associated with.": "The website this record is associated with.", "Invoice internal status (e.g. Draft, Open).": "Statut interne de la facture (par ex. Draft, Open).", - "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Remplace la liste déroulante Statut interne (par ex. draft, open, paid).", "Invoice line items. Each item is sent via JSON:API included.": "Lignes de facture. Chaque élément est envoyé via JSON:API included.", + "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}": "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}", + "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}": "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}", + "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}": "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}", "The customer this order belongs to.": "Le client auquel appartient cette commande.", "Unique order reference (e.g. FR1012401Z).": "Référence de commande unique (par ex. FR1012401Z).", "Purchase order number provided by the buyer.": "Numéro de commande d'achat fourni par l'acheteur.", @@ -128,50 +183,75 @@ "The shipping method type (e.g. \"primary\").": "Le type de méthode d'expédition (par ex. \"primary\").", "Prevent the promotions engine from running for this order.": "Empêcher le moteur de promotions de s'exécuter pour cette commande.", "Order internal status (e.g. Open, Cancelled).": "Statut interne de la commande (par ex. Open, Cancelled).", - "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Remplace la liste déroulante Statut interne (par ex. order_internal_status.open).", "Search payment terms.": "Rechercher des conditions de paiement.", - "Overrides the Payment Term dropdown.": "Remplace la liste déroulante Condition de paiement.", "Search warehouses.": "Rechercher des entrepôts.", - "Overrides the Warehouse dropdown.": "Remplace la liste déroulante Entrepôt.", "Search orders to use as the parent order.": "Rechercher des commandes à utiliser comme commande parente.", - "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Remplace la liste déroulante Commande parente. Collez un ID de commande d'une étape précédente.", "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Statut de commande géré par un système externe (pertinent uniquement lorsque \"Enable External Status Management\" est activé).", - "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Remplace la liste déroulante Statut. Appliqué uniquement lorsque \"Enable External Status Management\" est activé.", "Address label (e.g. \"Main Office\").": "Libellé d'adresse (par ex. \"Main Office\").", "ISO-3166 country. Start typing to filter the list.": "Pays ISO-3166. Commencez à taper pour filtrer la liste.", - "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Remplace la liste déroulante Pays de facturation (code pays ISO-3166 à 2 lettres, par ex. US).", "Region or state. Select a country first. Start typing to filter.": "Région ou État. Sélectionnez d'abord un pays. Commencez à taper pour filtrer.", - "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Remplace la liste déroulante Région de facturation (code ISO 3166-2, par ex. US-NY).", "Free-text region for countries without predefined regions.": "Région en texte libre pour les pays sans régions prédéfinies.", "Address label (e.g. \"Warehouse East\").": "Libellé d'adresse (par ex. \"Warehouse East\").", - "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Remplace la liste déroulante Pays d'expédition (code pays ISO-3166 à 2 lettres, par ex. US).", - "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Remplace la liste déroulante Région d'expédition (code ISO 3166-2, par ex. US-NY).", "Order line items.": "Lignes de commande.", "Search products.": "Rechercher des produits.", - "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Chemin relatif à la base de l'API, par ex. /orders, /orders/42, /invoices?filter[status]=open", - "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "En-têtes de requête sous forme d'objet JSON. Les valeurs par défaut incluent Accept et X-Include pour JSON:API.", - "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Paramètres de requête supplémentaires sous forme d'objet JSON, par ex. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", - "Request body as a JSON object (used for POST / PATCH).": "Corps de requête sous forme d'objet JSON (utilisé pour POST / PATCH).", - "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Le type de ressource JSON:API (par ex. orders, invoices, products). Le champ _type est automatiquement utilisé comme type de ressource.", - "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID de la ressource - laissez vide lors de la création d'un nouvel enregistrement (POST) ou lorsque l'entrée contient un champ id (le champ id est repris automatiquement).", - "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "L'objet plat à sérialiser. Passez ici le document JSON complet : les tableaux et objets sont automatiquement placés dans \"relationships\" ; seuls les scalaires deviennent des attributs. Pour une construction manuelle, utilisez des paires clé/valeur simples, par ex. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", - "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Carte de relations explicite facultative qui a priorité sur toute relation détectée dans Attributes. Chaque clé est un nom de relation ; la valeur est un objet de liaison unique {\"type\":\"customers\",\"id\":\"42\"}, un tableau d'objets de liaison ou null. Exemple : {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", - "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Tableau facultatif d'objets ressource JSON:API complets à intégrer dans la section \"included\" de la requête. Si l'entrée Attributes contient déjà un tableau \"included\" (par ex. le corps brut de la réponse Appel d'API), il sera transmis automatiquement ; ce champ ne doit être renseigné que lors de l'ajout ou du remplacement manuel de ressources incluses. Chaque élément doit être une ressource JSON:API valide avec les champs \"type\", \"id\" et \"attributes\".", - "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Le corps brut de la réponse JSON:API - le champ \"body\" de la sortie de l'action Appel d'API. Doit contenir une clé de premier niveau \"data\" (ressource unique ou collection).", + "A human-readable name that identifies the customer (company).": "A human-readable name that identifies the customer (company).", + "A unique identifier from an external system.": "A unique identifier from an external system.", + "Customer's value added tax identification number.": "Customer's value added tax identification number.", + "The parent company this customer (division) reports to.": "The parent company this customer (division) reports to.", + "Search customer groups.": "Search customer groups.", + "Search customer tax codes.": "Search customer tax codes.", + "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").": "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").", + "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", + "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", + "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", + "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", + "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", + "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", + "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", + "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", + "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", + "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", + "Updated email address of the customer user.": "Updated email address of the customer user.", + "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Enable or disable the storefront account.": "Enable or disable the storefront account.", + "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", + "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", + "Email address of the user.": "Email address of the user.", + "Job title or position.": "Job title or position.", + "When disabled the user cannot log in. Defaults to true.": "When disabled the user cannot log in. Defaults to true.", + "The business unit that owns this record.": "The business unit that owns this record.", + "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.": "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.", + "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.": "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.", + "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.": "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.", + "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.": "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.", + "Authentication status of the user (e.g. active, reset, locked).": "Authentication status of the user (e.g. active, reset, locked).", + "The numeric ID of the user to update.": "The numeric ID of the user to update.", + "Updated login name for the user.": "Updated login name for the user.", + "Updated email address of the user.": "Updated email address of the user.", + "Enable or disable the user account.": "Enable or disable the user account.", + "Search business units.": "Search business units.", + "Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.", + "Enable for files like PDFs, images, etc.": "Enable for files like PDFs, images, etc.", + "e.g. orders, invoices, products. Taken from _type when left empty.": "e.g. orders, invoices, products. Taken from _type when left empty.", + "Leave empty to create a record, or when the input carries an id.": "Leave empty to create a record, or when the input carries an id.", + "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.": "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.", + "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", + "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", + "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", "GET": "GET", "POST": "POST", "PATCH": "PATCH", + "PUT": "PUT", "DELETE": "DELETE", - "New Order": "Nouvelle commande", - "Order Removal": "Suppression de commande", - "Order Update": "Mise à jour de commande", - "New Invoice": "Nouvelle facture", - "Invoice Removal": "Suppression de facture", - "Invoice Update": "Mise à jour de facture", - "Triggered when a new order is created": "Déclenché lorsqu'une nouvelle commande est créée", - "Triggered when an order is deleted": "Déclenché lorsqu'une commande est supprimée", - "Triggered when an order is updated": "Déclenché lorsqu'une commande est mise à jour", - "Triggered when a new invoice is created": "Déclenché lorsqu'une nouvelle facture est créée", - "Triggered when an invoice is deleted": "Déclenché lorsqu'une facture est supprimée", - "Triggered when an invoice is updated": "Déclenché lorsqu'une facture est mise à jour" + "HEAD": "HEAD", + "None": "None", + "JSON": "JSON", + "Form Data": "Form Data", + "Raw": "Raw", + "Oro Webhook Event": "Oro Webhook Event", + "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", + "Topic": "Topic", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" } diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json index 86e06d6f89d1..4eb3295ac5f5 100644 --- a/packages/pieces/community/orocommerce/src/i18n/nl.json +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -4,21 +4,37 @@ "Admin Prefix": "Admin-prefix", "Client ID": "Client-ID", "Client Secret": "Clientgeheim", + "Default HTTP Headers": "Default HTTP Headers", + "Internal infrastructure": "Internal infrastructure", "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "De basis-URL van uw OroCommerce-instantie (bijv. https://your-store.com).", "The admin panel URL prefix (default is \"admin\").": "Het URL-prefix van het beheerpaneel (standaard \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "De OAuth Client-ID uit uw OroCommerce OAuth-applicatie.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Het OAuth Clientgeheim uit uw OroCommerce OAuth-applicatie.", + "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthenticeer bij de OroCommerce-API's met OAuth 2.0 Client Credentials.\n\n**Stappen om referenties te verkrijgen:**\n1. Meld u aan bij uw OroCommerce-beheerpaneel.\n2. Ga naar **System** > **User Management** > **OAuth Applications**.\n3. Klik op **Create OAuth Application** en configureer:\n - **Application Name**: Voer een beschrijvende naam in (bijv. \"Activepieces Integration\")\n - **Grants**: Selecteer **Client Credentials**\n - **Redirect URIs**: Niet vereist voor de Client Credentials-flow\n4. Sla de applicatie op en kopieer de **Client ID** en **Client Secret**.\n5. Noteer uw **Server URL** (bijv. `https://your-store.com`) en **Admin Prefix** (meestal `admin`).\n ", "Create Invoice": "Factuur aanmaken", "Create Order": "Order aanmaken", - "API Call": "API-oproep", + "Create Customer": "Create Customer", + "Update Customer": "Update Customer", + "Create Customer User": "Create Customer User", + "Update Customer User": "Update Customer User", + "Create User": "Create User", + "Update User": "Update User", + "Custom API Call": "Custom API Call", "Serialize JSON:API Request": "JSON:API-verzoek serialiseren", "Unserialize JSON:API Response": "JSON:API-respons deserialiseren", "Creates a new invoice record in OroCommerce.": "Maakt een nieuw factuurrecord aan in OroCommerce.", "Creates a new order record in OroCommerce.": "Maakt een nieuw orderrecord aan in OroCommerce.", + "Creates a new customer (company) record in OroCommerce.": "Creates a new customer (company) record in OroCommerce.", + "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.": "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.", + "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", + "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", + "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Voer een directe geauthenticeerde oproep uit naar de OroCommerce JSON:API.", - "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Zet een eenvoudig object om in een JSON:API-conforme requestbody. Relatievelden worden automatisch gedetecteerd: elke array- of objectwaarde wordt als relatie behandeld, alleen scalars worden attributen. Het resultaat kan direct worden gebruikt als requestbody van de API Call-actie.", - "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Maakt een JSON:API-responsbody vlak (naar een eenvoudig object of een array van objecten). Inbegrepen relaties worden opgelost en inline opgenomen. Onopgeloste referenties naar relaties worden bewaard als { _type, id }-stubs zodat het document opnieuw kan worden geserialiseerd naar een geldige JSON:API-aanvraag zonder datageverlies. Geef de uitvoer \"body\" van de API Call-actie door als Response-invoer.", + "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", + "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", "Invoice Date": "Factuurdatum", "Currency": "Valuta", "Customer Name": "Naam klant", @@ -36,11 +52,16 @@ "Shipping Method": "Verzendmethode", "Seller Info": "Verkoperinfo", "External Payment URL": "Externe betalings-URL", + "Invoice PDF": "Invoice PDF", + "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organisatie", - "User": "Gebruiker", + "Owner": "Owner", "Website": "Website", "Internal Status": "Interne status", "Line Items": "Regelitems", + "Additional Attributes": "Additional Attributes", + "Additional Relations": "Additional Relations", + "Additional Headers": "Additional Headers", "Identifier": "Identificatie", "PO Number": "Inkoopordernummer", "Customer Notes": "Klantnotities", @@ -78,11 +99,46 @@ "Shipping: Region / State": "Verzending: regio / staat", "Shipping: Custom Region": "Verzending: aangepaste regio", "Product": "Product", + "Name": "Name", + "External ID": "External ID", + "VAT ID": "VAT ID", + "Parent Customer": "Parent Customer", + "Customer Group": "Customer Group", + "Tax Code": "Tax Code", + "Internal Rating": "Internal Rating", + "Addresses": "Addresses", + "Customer ID": "Customer ID", + "Email": "Email", + "Password": "Password", + "Name Prefix": "Name Prefix", + "First Name": "First Name", + "Middle Name": "Middle Name", + "Last Name": "Last Name", + "Name Suffix": "Name Suffix", + "Enabled": "Enabled", + "Confirmed": "Confirmed", + "Birthday": "Birthday", + "Roles (replaces all existing roles)": "Roles (replaces all existing roles)", + "Customer User ID": "Customer User ID", + "Username": "Username", + "Phone": "Phone", + "Owner (Business Unit)": "Owner (Business Unit)", + "Business Units (replaces all existing business units)": "Business Units (replaces all existing business units)", + "User Roles (replaces all existing roles)": "User Roles (replaces all existing roles)", + "Organizations (replaces all existing organizations)": "Organizations (replaces all existing organizations)", + "User Groups (replaces all existing groups)": "User Groups (replaces all existing groups)", + "Auth Status": "Auth Status", + "User ID": "User ID", + "Business Unit": "Business Unit", "Method": "Methode", - "Resource URI": "Resource-URI", "Headers": "Headers", "Query Parameters": "Queryparameters", - "Request Body": "Requestbody", + "Body Type": "Body Type", + "Body": "Body", + "Response is Binary ?": "Response is Binary ?", + "No Error on Failure": "No Error on Failure", + "Timeout (in seconds)": "Timeout (in seconds)", + "Follow redirects": "Follow redirects", "Resource Type": "Resourcetype", "Resource ID": "Resource-ID", "Attributes": "Attributen", @@ -93,9 +149,7 @@ "ISO-4217 3-letter currency code (e.g. USD, EUR).": "ISO-4217 valutacode van 3 letters (bijv. USD, EUR).", "Name of the company being billed. Stored as a plain-text label on the invoice.": "Naam van het bedrijf dat wordt gefactureerd. Wordt als plattetekstlabel op de factuur opgeslagen.", "Select a customer.": "Selecteer een klant.", - "Overrides the Customer dropdown. Paste an ID from a previous step.": "Overschrijft de klantkeuzelijst. Plak een ID uit een vorige stap.", "Select a Customer User.": "Selecteer een klantgebruiker.", - "Overrides the Customer User dropdown.": "Overschrijft de klantgebruikerskeuzelijst.", "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Een optionele ID-referentie naar een klant. Kan worden gebruikt om een willekeurige externe ID op te slaan.", "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Een optionele ID-referentie naar een klantgebruiker. Kan worden gebruikt om een willekeurige externe ID op te slaan.", "Total invoice amount. Should equal the sum of all line item row totals.": "Totaal factuurbedrag. Moet gelijk zijn aan de som van alle regelitemtotalen.", @@ -108,15 +162,16 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Label voor verzendmethode (bijv. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "HTML-tekenreeks voor verkoperscontact/adres.", "URL for the external payment page.": "URL voor de externe betalingspagina.", - "Search organizations.": "Zoek organisaties.", - "Overrides the Organization dropdown.": "Overschrijft de organisatiekeuzelijst.", - "Search user by name, username or email.": "Zoek gebruiker op naam, gebruikersnaam of e-mailadres.", - "Overrides the Owner dropdown.": "Overschrijft de eigenaar-keuzelijst.", - "Search websites.": "Zoek websites.", - "Overrides the Website dropdown.": "Overschrijft de websitekeuzelijst.", + "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", + "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "The organization this record belongs to.": "The organization this record belongs to.", + "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", + "The website this record is associated with.": "The website this record is associated with.", "Invoice internal status (e.g. Draft, Open).": "Interne factuurstatus (bijv. Draft, Open).", - "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Overschrijft de keuzelijst voor interne status (bijv. draft, open, paid).", "Invoice line items. Each item is sent via JSON:API included.": "Factuurregelitems. Elk item wordt als JSON:API included verzonden.", + "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}": "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}", + "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}": "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}", + "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}": "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}", "The customer this order belongs to.": "De klant waartoe deze order behoort.", "Unique order reference (e.g. FR1012401Z).": "Unieke orderreferentie (bijv. FR1012401Z).", "Purchase order number provided by the buyer.": "Inkoopordernummer opgegeven door de koper.", @@ -128,51 +183,75 @@ "The shipping method type (e.g. \"primary\").": "Het type verzendmethode (bijv. \"primary\").", "Prevent the promotions engine from running for this order.": "Voorkom dat de promotie-engine wordt uitgevoerd voor deze order.", "Order internal status (e.g. Open, Cancelled).": "Interne orderstatus (bijv. Open, Cancelled).", - "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Overschrijft de interne-statuskeuzelijst (bijv. order_internal_status.open).", "Search payment terms.": "Zoek betalingstermijnen.", - "Overrides the Payment Term dropdown.": "Overschrijft de betalingstermijn-keuzelijst.", "Search warehouses.": "Zoek magazijnen.", - "Overrides the Warehouse dropdown.": "Overschrijft de magazijn-keuzelijst.", "Search orders to use as the parent order.": "Zoek orders om als bovenliggende order te gebruiken.", - "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Overschrijft de bovenliggende-order-keuzelijst. Plak een order-ID uit een vorige stap.", "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Orderstatus beheerd door een extern systeem (alleen relevant wanneer \"Enable External Status Management\" is ingeschakeld).", - "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Overschrijft de statuskeuzelijst. Wordt alleen toegepast als \"Enable External Status Management\" is ingeschakeld.", "Address label (e.g. \"Main Office\").": "Adreslabel (bijv. \"Main Office\").", "ISO-3166 country. Start typing to filter the list.": "ISO-3166-land. Begin te typen om de lijst te filteren.", - "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overschrijft de landkeuzelijst voor facturatie (ISO-3166-landcode met 2 letters, bijv. US).", "Region or state. Select a country first. Start typing to filter.": "Regio of staat. Selecteer eerst een land. Begin te typen om te filteren.", - "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overschrijft de regio-keuzelijst voor facturatie (ISO 3166-2-code, bijv. US-NY).", "Free-text region for countries without predefined regions.": "Vrijetekstregio voor landen zonder vooraf gedefinieerde regio's.", "Address label (e.g. \"Warehouse East\").": "Adreslabel (bijv. \"Warehouse East\").", - "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overschrijft de landkeuzelijst voor verzending (ISO-3166-landcode met 2 letters, bijv. US).", - "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overschrijft de regiokeuzelijst voor verzending (ISO 3166-2-code, bijv. US-NY).", "Order line items.": "Orderregelitems.", "Search products.": "Zoek producten.", - "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Pad relatief ten opzichte van de API-basis, bijv. /orders, /orders/42, /invoices?filter[status]=open", - "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Requestheaders als JSON-object. Standaardwaarden omvatten Accept en X-Include voor JSON:API.", - "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Aanvullende queryparameters als JSON-object, bijv. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", - "Request body as a JSON object (used for POST / PATCH).": "Requestbody als JSON-object (gebruikt voor POST / PATCH).", - "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Het JSON:API-resourcetype (bijv. orders, invoices, products). Het veld _type wordt automatisch als resourcetype gebruikt.", - "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID van de resource - laat leeg bij het aanmaken van een nieuw record (POST) of wanneer de invoer een id-veld bevat (het id-veld wordt automatisch opgepikt).", - "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Het vlakke object dat moet worden geserialiseerd. Geef hier het volledige JSON-document door - arrays en objecten worden automatisch in \"relationships\" geplaatst; alleen scalars worden attributen. Voor handmatige opbouw gebruikt u eenvoudige sleutel/waarde-paren, bijv. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", - "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Optionele expliciete relatietoewijzing die voorrang heeft op relaties die in Attributes worden gedetecteerd. Elke sleutel is een relatienaam; de waarde is een enkel koppelingsobject {\"type\":\"customers\",\"id\":\"42\"}, een array met koppelingsobjecten of null. Voorbeeld: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", - "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Optionele array met volledige JSON:API-resourceobjecten om op te nemen in de sectie \"included\" van het verzoek. Als de invoer van Attributes al een array \"included\" bevat (bijv. de ruwe API Call-responsbody), wordt deze automatisch doorgegeven - dit veld hoeft alleen te worden ingesteld bij het handmatig toevoegen of overschrijven van opgenomen resources. Elk item moet een geldige JSON:API-resource zijn met de velden \"type\", \"id\" en \"attributes\".", - "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "De ruwe JSON:API-responsbody - het veld \"body\" van de uitvoer van de API Call-actie. Moet een bovenliggende sleutel \"data\" bevatten (één resource of een collectie).", + "A human-readable name that identifies the customer (company).": "A human-readable name that identifies the customer (company).", + "A unique identifier from an external system.": "A unique identifier from an external system.", + "Customer's value added tax identification number.": "Customer's value added tax identification number.", + "The parent company this customer (division) reports to.": "The parent company this customer (division) reports to.", + "Search customer groups.": "Search customer groups.", + "Search customer tax codes.": "Search customer tax codes.", + "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").": "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").", + "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", + "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", + "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", + "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", + "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", + "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", + "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", + "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", + "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", + "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", + "Updated email address of the customer user.": "Updated email address of the customer user.", + "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Enable or disable the storefront account.": "Enable or disable the storefront account.", + "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", + "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", + "Email address of the user.": "Email address of the user.", + "Job title or position.": "Job title or position.", + "When disabled the user cannot log in. Defaults to true.": "When disabled the user cannot log in. Defaults to true.", + "The business unit that owns this record.": "The business unit that owns this record.", + "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.": "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.", + "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.": "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.", + "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.": "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.", + "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.": "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.", + "Authentication status of the user (e.g. active, reset, locked).": "Authentication status of the user (e.g. active, reset, locked).", + "The numeric ID of the user to update.": "The numeric ID of the user to update.", + "Updated login name for the user.": "Updated login name for the user.", + "Updated email address of the user.": "Updated email address of the user.", + "Enable or disable the user account.": "Enable or disable the user account.", + "Search business units.": "Search business units.", + "Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.", + "Enable for files like PDFs, images, etc.": "Enable for files like PDFs, images, etc.", + "e.g. orders, invoices, products. Taken from _type when left empty.": "e.g. orders, invoices, products. Taken from _type when left empty.", + "Leave empty to create a record, or when the input carries an id.": "Leave empty to create a record, or when the input carries an id.", + "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.": "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.", + "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", + "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", + "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", "GET": "GET", "POST": "POST", "PATCH": "PATCH", + "PUT": "PUT", "DELETE": "DELETE", - "New Order": "Nieuwe order", - "Order Removal": "Orderverwijdering", - "Order Update": "Orderupdate", - "New Invoice": "Nieuwe factuur", - "Invoice Removal": "Factuurverwijdering", - "Invoice Update": "Factuurupdate", - "Triggered when a new order is created": "Wordt geactiveerd wanneer een nieuwe order wordt aangemaakt", - "Triggered when an order is deleted": "Wordt geactiveerd wanneer een order wordt verwijderd", - "Triggered when an order is updated": "Wordt geactiveerd wanneer een order wordt bijgewerkt", - "Triggered when a new invoice is created": "Wordt geactiveerd wanneer een nieuwe factuur wordt aangemaakt", - "Triggered when an invoice is deleted": "Wordt geactiveerd wanneer een factuur wordt verwijderd", - "Triggered when an invoice is updated": "Wordt geactiveerd wanneer een factuur wordt bijgewerkt" + "HEAD": "HEAD", + "None": "None", + "JSON": "JSON", + "Form Data": "Form Data", + "Raw": "Raw", + "Oro Webhook Event": "Oro Webhook Event", + "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", + "Topic": "Topic", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" } - diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json index 6cabe22d8638..c62458c9c113 100644 --- a/packages/pieces/community/orocommerce/src/i18n/pl.json +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -4,21 +4,37 @@ "Admin Prefix": "Prefiks panelu administracyjnego", "Client ID": "Identyfikator klienta", "Client Secret": "Sekret klienta", + "Default HTTP Headers": "Default HTTP Headers", + "Internal infrastructure": "Internal infrastructure", "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "Bazowy adres URL Twojej instancji OroCommerce (np. https://your-store.com).", "The admin panel URL prefix (default is \"admin\").": "Prefiks URL panelu administracyjnego (domyślnie \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "Identyfikator klienta OAuth z Twojej aplikacji OAuth w OroCommerce.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Sekret klienta OAuth z Twojej aplikacji OAuth w OroCommerce.", + "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nUwierzytelnij się w API OroCommerce, używając danych klienta OAuth 2.0 (Client Credentials).\n\n**Kroki, aby uzyskać dane logowania:**\n1. Zaloguj się do panelu administracyjnego OroCommerce.\n2. Przejdź do **System** > **User Management** > **OAuth Applications**.\n3. Kliknij **Create OAuth Application** i skonfiguruj:\n - **Application Name**: Wpisz opisową nazwę (np. \"Activepieces Integration\")\n - **Grants**: Wybierz **Client Credentials**\n - **Redirect URIs**: Niewymagane dla przepływu Client Credentials\n4. Zapisz aplikację i skopiuj wartości **Client ID** oraz **Client Secret**.\n5. Zanotuj **Server URL** (np. `https://your-store.com`) oraz **Admin Prefix** (zwykle `admin`).\n ", "Create Invoice": "Utwórz fakturę", "Create Order": "Utwórz zamówienie", - "API Call": "Wywołanie API", + "Create Customer": "Create Customer", + "Update Customer": "Update Customer", + "Create Customer User": "Create Customer User", + "Update Customer User": "Update Customer User", + "Create User": "Create User", + "Update User": "Update User", + "Custom API Call": "Custom API Call", "Serialize JSON:API Request": "Serializuj żądanie JSON:API", "Unserialize JSON:API Response": "Deserializuj odpowiedź JSON:API", "Creates a new invoice record in OroCommerce.": "Tworzy nowy rekord faktury w OroCommerce.", "Creates a new order record in OroCommerce.": "Tworzy nowy rekord zamówienia w OroCommerce.", + "Creates a new customer (company) record in OroCommerce.": "Creates a new customer (company) record in OroCommerce.", + "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.": "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.", + "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", + "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", + "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Wykonuje bezpośrednie, uwierzytelnione wywołanie do OroCommerce JSON:API.", - "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Konwertuje zwykły obiekt na treść żądania zgodną z JSON:API. Pola relacji są wykrywane automatycznie — każda wartość będąca tablicą lub obiektem jest traktowana jako relacja, a tylko wartości skalarne stają się atrybutami. Wynik można przekazać bezpośrednio jako treść żądania w akcji Wywołanie API.", - "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Spłaszcza treść odpowiedzi JSON:API do zwykłego obiektu (lub tablicy obiektów). Relacje z sekcji included są rozwiązywane i wstawiane inline. Nierozwiązane odwołania do relacji są zachowywane jako zaślepki { _type, id }, aby dokument można było ponownie zserializować do poprawnego żądania JSON:API bez utraty danych. Przekaż wynik \"body\" z akcji Wywołanie API jako wejście Response.", + "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", + "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", "Invoice Date": "Data faktury", "Currency": "Waluta", "Customer Name": "Nazwa klienta", @@ -36,11 +52,16 @@ "Shipping Method": "Metoda wysyłki", "Seller Info": "Informacje o sprzedawcy", "External Payment URL": "Zewnętrzny URL płatności", + "Invoice PDF": "Invoice PDF", + "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organizacja", - "User": "Użytkownik", + "Owner": "Owner", "Website": "Strona internetowa", "Internal Status": "Status wewnętrzny", "Line Items": "Pozycje", + "Additional Attributes": "Additional Attributes", + "Additional Relations": "Additional Relations", + "Additional Headers": "Additional Headers", "Identifier": "Identyfikator", "PO Number": "Numer zamówienia zakupu (PO)", "Customer Notes": "Uwagi klienta", @@ -78,11 +99,46 @@ "Shipping: Region / State": "Wysyłka: region / województwo", "Shipping: Custom Region": "Wysyłka: własny region", "Product": "Produkt", + "Name": "Name", + "External ID": "External ID", + "VAT ID": "VAT ID", + "Parent Customer": "Parent Customer", + "Customer Group": "Customer Group", + "Tax Code": "Tax Code", + "Internal Rating": "Internal Rating", + "Addresses": "Addresses", + "Customer ID": "Customer ID", + "Email": "Email", + "Password": "Password", + "Name Prefix": "Name Prefix", + "First Name": "First Name", + "Middle Name": "Middle Name", + "Last Name": "Last Name", + "Name Suffix": "Name Suffix", + "Enabled": "Enabled", + "Confirmed": "Confirmed", + "Birthday": "Birthday", + "Roles (replaces all existing roles)": "Roles (replaces all existing roles)", + "Customer User ID": "Customer User ID", + "Username": "Username", + "Phone": "Phone", + "Owner (Business Unit)": "Owner (Business Unit)", + "Business Units (replaces all existing business units)": "Business Units (replaces all existing business units)", + "User Roles (replaces all existing roles)": "User Roles (replaces all existing roles)", + "Organizations (replaces all existing organizations)": "Organizations (replaces all existing organizations)", + "User Groups (replaces all existing groups)": "User Groups (replaces all existing groups)", + "Auth Status": "Auth Status", + "User ID": "User ID", + "Business Unit": "Business Unit", "Method": "Metoda", - "Resource URI": "URI zasobu", "Headers": "Nagłówki", "Query Parameters": "Parametry zapytania", - "Request Body": "Treść żądania", + "Body Type": "Body Type", + "Body": "Body", + "Response is Binary ?": "Response is Binary ?", + "No Error on Failure": "No Error on Failure", + "Timeout (in seconds)": "Timeout (in seconds)", + "Follow redirects": "Follow redirects", "Resource Type": "Typ zasobu", "Resource ID": "ID zasobu", "Attributes": "Atrybuty", @@ -93,9 +149,7 @@ "ISO-4217 3-letter currency code (e.g. USD, EUR).": "3-literowy kod waluty ISO-4217 (np. USD, EUR).", "Name of the company being billed. Stored as a plain-text label on the invoice.": "Nazwa firmy, na którą wystawiana jest faktura. Zapisywana jako etykieta w postaci zwykłego tekstu na fakturze.", "Select a customer.": "Wybierz klienta.", - "Overrides the Customer dropdown. Paste an ID from a previous step.": "Nadpisuje listę rozwijaną Klient. Wklej ID z poprzedniego kroku.", "Select a Customer User.": "Wybierz użytkownika klienta.", - "Overrides the Customer User dropdown.": "Nadpisuje listę rozwijaną Użytkownik klienta.", "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Opcjonalne odwołanie ID do klienta. Może służyć do przechowywania dowolnego zewnętrznego identyfikatora.", "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Opcjonalne odwołanie ID do użytkownika klienta. Może służyć do przechowywania dowolnego zewnętrznego identyfikatora.", "Total invoice amount. Should equal the sum of all line item row totals.": "Łączna kwota faktury. Powinna być równa sumie wartości łącznych wszystkich pozycji.", @@ -108,15 +162,16 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Etykieta metody wysyłki (np. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "Ciąg HTML kontaktu/adresu sprzedawcy.", "URL for the external payment page.": "Adres URL zewnętrznej strony płatności.", - "Search organizations.": "Wyszukaj organizacje.", - "Overrides the Organization dropdown.": "Nadpisuje listę rozwijaną Organizacja.", - "Search user by name, username or email.": "Wyszukaj użytkownika po imieniu, nazwie użytkownika lub adresie e-mail.", - "Overrides the Owner dropdown.": "Nadpisuje listę rozwijaną Właściciel.", - "Search websites.": "Wyszukaj strony internetowe.", - "Overrides the Website dropdown.": "Nadpisuje listę rozwijaną Strona internetowa.", + "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", + "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "The organization this record belongs to.": "The organization this record belongs to.", + "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", + "The website this record is associated with.": "The website this record is associated with.", "Invoice internal status (e.g. Draft, Open).": "Wewnętrzny status faktury (np. Draft, Open).", - "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Nadpisuje listę rozwijaną Status wewnętrzny (np. draft, open, paid).", "Invoice line items. Each item is sent via JSON:API included.": "Pozycje faktury. Każda pozycja jest wysyłana jako JSON:API included.", + "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}": "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}", + "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}": "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}", + "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}": "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}", "The customer this order belongs to.": "Klient, do którego należy to zamówienie.", "Unique order reference (e.g. FR1012401Z).": "Unikalny identyfikator zamówienia (np. FR1012401Z).", "Purchase order number provided by the buyer.": "Numer zamówienia zakupu podany przez kupującego.", @@ -128,50 +183,75 @@ "The shipping method type (e.g. \"primary\").": "Typ metody wysyłki (np. \"primary\").", "Prevent the promotions engine from running for this order.": "Zablokuj uruchamianie silnika promocji dla tego zamówienia.", "Order internal status (e.g. Open, Cancelled).": "Wewnętrzny status zamówienia (np. Open, Cancelled).", - "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Nadpisuje listę rozwijaną Status wewnętrzny (np. order_internal_status.open).", "Search payment terms.": "Wyszukaj terminy płatności.", - "Overrides the Payment Term dropdown.": "Nadpisuje listę rozwijaną Termin płatności.", "Search warehouses.": "Wyszukaj magazyny.", - "Overrides the Warehouse dropdown.": "Nadpisuje listę rozwijaną Magazyn.", "Search orders to use as the parent order.": "Wyszukaj zamówienia do użycia jako zamówienie nadrzędne.", - "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Nadpisuje listę rozwijaną Zamówienie nadrzędne. Wklej ID zamówienia z poprzedniego kroku.", "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Status zamówienia zarządzany przez system zewnętrzny (istotne tylko, gdy opcja \"Enable External Status Management\" jest włączona).", - "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Nadpisuje listę rozwijaną Status. Stosowane tylko, gdy opcja \"Enable External Status Management\" jest włączona.", "Address label (e.g. \"Main Office\").": "Etykieta adresu (np. \"Main Office\").", "ISO-3166 country. Start typing to filter the list.": "Kraj ISO-3166. Zacznij pisać, aby filtrować listę.", - "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Nadpisuje listę rozwijaną Kraj rozliczeniowy (dwuliterowy kod ISO-3166, np. US).", "Region or state. Select a country first. Start typing to filter.": "Region lub województwo. Najpierw wybierz kraj. Zacznij pisać, aby filtrować.", - "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Nadpisuje listę rozwijaną Region rozliczeniowy (kod ISO 3166-2, np. US-NY).", "Free-text region for countries without predefined regions.": "Region wprowadzany ręcznie dla krajów bez zdefiniowanych regionów.", "Address label (e.g. \"Warehouse East\").": "Etykieta adresu (np. \"Warehouse East\").", - "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Nadpisuje listę rozwijaną Kraj dostawy (dwuliterowy kod ISO-3166, np. US).", - "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Nadpisuje listę rozwijaną Region dostawy (kod ISO 3166-2, np. US-NY).", "Order line items.": "Pozycje zamówienia.", "Search products.": "Wyszukaj produkty.", - "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Ścieżka względna względem bazowego adresu API, np. /orders, /orders/42, /invoices?filter[status]=open", - "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Nagłówki żądania jako obiekt JSON. Domyślnie zawiera Accept oraz X-Include dla JSON:API.", - "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Dodatkowe parametry zapytania jako obiekt JSON, np. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", - "Request body as a JSON object (used for POST / PATCH).": "Treść żądania jako obiekt JSON (używane dla POST / PATCH).", - "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Typ zasobu JSON:API (np. orders, invoices, products). Pole _type jest automatycznie używane jako typ zasobu.", - "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID zasobu — pozostaw puste podczas tworzenia nowego rekordu (POST) lub gdy dane wejściowe zawierają pole id (pole id zostanie wykryte automatycznie).", - "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Spłaszczony obiekt do serializacji. Przekaż tutaj pełny dokument JSON — tablice i obiekty są automatycznie umieszczane w \"relationships\"; tylko wartości skalarne stają się atrybutami. Do ręcznej konstrukcji użyj prostych par klucz/wartość, np. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", - "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Opcjonalna jawna mapa relacji, która ma pierwszeństwo przed relacjami wykrytymi w Attributes. Każdy klucz to nazwa relacji; wartość to pojedynczy obiekt powiązania {\"type\":\"customers\",\"id\":\"42\"}, tablica obiektów powiązania lub null. Przykład: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", - "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Opcjonalna tablica pełnych obiektów zasobów JSON:API do osadzenia w sekcji \"included\" żądania. Jeśli dane wejściowe Attributes już zawierają tablicę \"included\" (np. surową treść odpowiedzi z Wywołania API), zostanie ona przekazana automatycznie — to pole należy ustawić tylko przy ręcznym dodawaniu lub nadpisywaniu zasobów included. Każdy element musi być poprawnym zasobem JSON:API z polami \"type\", \"id\" oraz \"attributes\".", - "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Surowa treść odpowiedzi JSON:API — pole \"body\" z wyniku akcji Wywołanie API. Musi zawierać klucz najwyższego poziomu \"data\" (pojedynczy zasób lub kolekcję).", + "A human-readable name that identifies the customer (company).": "A human-readable name that identifies the customer (company).", + "A unique identifier from an external system.": "A unique identifier from an external system.", + "Customer's value added tax identification number.": "Customer's value added tax identification number.", + "The parent company this customer (division) reports to.": "The parent company this customer (division) reports to.", + "Search customer groups.": "Search customer groups.", + "Search customer tax codes.": "Search customer tax codes.", + "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").": "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").", + "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", + "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", + "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", + "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", + "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", + "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", + "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", + "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", + "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", + "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", + "Updated email address of the customer user.": "Updated email address of the customer user.", + "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Enable or disable the storefront account.": "Enable or disable the storefront account.", + "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", + "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", + "Email address of the user.": "Email address of the user.", + "Job title or position.": "Job title or position.", + "When disabled the user cannot log in. Defaults to true.": "When disabled the user cannot log in. Defaults to true.", + "The business unit that owns this record.": "The business unit that owns this record.", + "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.": "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.", + "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.": "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.", + "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.": "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.", + "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.": "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.", + "Authentication status of the user (e.g. active, reset, locked).": "Authentication status of the user (e.g. active, reset, locked).", + "The numeric ID of the user to update.": "The numeric ID of the user to update.", + "Updated login name for the user.": "Updated login name for the user.", + "Updated email address of the user.": "Updated email address of the user.", + "Enable or disable the user account.": "Enable or disable the user account.", + "Search business units.": "Search business units.", + "Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.", + "Enable for files like PDFs, images, etc.": "Enable for files like PDFs, images, etc.", + "e.g. orders, invoices, products. Taken from _type when left empty.": "e.g. orders, invoices, products. Taken from _type when left empty.", + "Leave empty to create a record, or when the input carries an id.": "Leave empty to create a record, or when the input carries an id.", + "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.": "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.", + "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", + "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", + "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", "GET": "GET", "POST": "POST", "PATCH": "PATCH", + "PUT": "PUT", "DELETE": "DELETE", - "New Order": "Nowe zamówienie", - "Order Removal": "Usunięcie zamówienia", - "Order Update": "Aktualizacja zamówienia", - "New Invoice": "Nowa faktura", - "Invoice Removal": "Usunięcie faktury", - "Invoice Update": "Aktualizacja faktury", - "Triggered when a new order is created": "Wyzwalane, gdy zostanie utworzone nowe zamówienie", - "Triggered when an order is deleted": "Wyzwalane, gdy zamówienie zostanie usunięte", - "Triggered when an order is updated": "Wyzwalane, gdy zamówienie zostanie zaktualizowane", - "Triggered when a new invoice is created": "Wyzwalane, gdy zostanie utworzona nowa faktura", - "Triggered when an invoice is deleted": "Wyzwalane, gdy faktura zostanie usunięta", - "Triggered when an invoice is updated": "Wyzwalane, gdy faktura zostanie zaktualizowana" + "HEAD": "HEAD", + "None": "None", + "JSON": "JSON", + "Form Data": "Form Data", + "Raw": "Raw", + "Oro Webhook Event": "Oro Webhook Event", + "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", + "Topic": "Topic", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" } diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json index ef43911e5bbe..c3f28eac6796 100644 --- a/packages/pieces/community/orocommerce/src/i18n/translation.json +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -4,21 +4,37 @@ "Admin Prefix": "Admin Prefix", "Client ID": "Client ID", "Client Secret": "Client Secret", + "Default HTTP Headers": "Default HTTP Headers", + "Internal infrastructure": "Internal infrastructure", "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "The base URL of your OroCommerce instance (e.g., https://your-store.com).", "The admin panel URL prefix (default is \"admin\").": "The admin panel URL prefix (default is \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "The OAuth Client ID from your OroCommerce OAuth application.", "The OAuth Client Secret from your OroCommerce OAuth application.": "The OAuth Client Secret from your OroCommerce OAuth application.", + "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **Client ID** and **Client Secret**.\n5. Note your **Server URL** (e.g., `https://your-store.com`) and **Admin Prefix** (usually `admin`).\n ", "Create Invoice": "Create Invoice", "Create Order": "Create Order", - "API Call": "API Call", + "Create Customer": "Create Customer", + "Update Customer": "Update Customer", + "Create Customer User": "Create Customer User", + "Update Customer User": "Update Customer User", + "Create User": "Create User", + "Update User": "Update User", + "Custom API Call": "Custom API Call", "Serialize JSON:API Request": "Serialize JSON:API Request", "Unserialize JSON:API Response": "Unserialize JSON:API Response", "Creates a new invoice record in OroCommerce.": "Creates a new invoice record in OroCommerce.", "Creates a new order record in OroCommerce.": "Creates a new order record in OroCommerce.", + "Creates a new customer (company) record in OroCommerce.": "Creates a new customer (company) record in OroCommerce.", + "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.": "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.", + "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", + "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", + "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Make a direct authenticated call to the OroCommerce JSON:API.", - "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.", - "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.", + "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", + "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", "Invoice Date": "Invoice Date", "Currency": "Currency", "Customer Name": "Customer Name", @@ -36,11 +52,16 @@ "Shipping Method": "Shipping Method", "Seller Info": "Seller Info", "External Payment URL": "External Payment URL", + "Invoice PDF": "Invoice PDF", + "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organization", - "User": "User", + "Owner": "Owner", "Website": "Website", "Internal Status": "Internal Status", "Line Items": "Line Items", + "Additional Attributes": "Additional Attributes", + "Additional Relations": "Additional Relations", + "Additional Headers": "Additional Headers", "Identifier": "Identifier", "PO Number": "PO Number", "Customer Notes": "Customer Notes", @@ -78,11 +99,46 @@ "Shipping: Region / State": "Shipping: Region / State", "Shipping: Custom Region": "Shipping: Custom Region", "Product": "Product", + "Name": "Name", + "External ID": "External ID", + "VAT ID": "VAT ID", + "Parent Customer": "Parent Customer", + "Customer Group": "Customer Group", + "Tax Code": "Tax Code", + "Internal Rating": "Internal Rating", + "Addresses": "Addresses", + "Customer ID": "Customer ID", + "Email": "Email", + "Password": "Password", + "Name Prefix": "Name Prefix", + "First Name": "First Name", + "Middle Name": "Middle Name", + "Last Name": "Last Name", + "Name Suffix": "Name Suffix", + "Enabled": "Enabled", + "Confirmed": "Confirmed", + "Birthday": "Birthday", + "Roles (replaces all existing roles)": "Roles (replaces all existing roles)", + "Customer User ID": "Customer User ID", + "Username": "Username", + "Phone": "Phone", + "Owner (Business Unit)": "Owner (Business Unit)", + "Business Units (replaces all existing business units)": "Business Units (replaces all existing business units)", + "User Roles (replaces all existing roles)": "User Roles (replaces all existing roles)", + "Organizations (replaces all existing organizations)": "Organizations (replaces all existing organizations)", + "User Groups (replaces all existing groups)": "User Groups (replaces all existing groups)", + "Auth Status": "Auth Status", + "User ID": "User ID", + "Business Unit": "Business Unit", "Method": "Method", - "Resource URI": "Resource URI", "Headers": "Headers", "Query Parameters": "Query Parameters", - "Request Body": "Request Body", + "Body Type": "Body Type", + "Body": "Body", + "Response is Binary ?": "Response is Binary ?", + "No Error on Failure": "No Error on Failure", + "Timeout (in seconds)": "Timeout (in seconds)", + "Follow redirects": "Follow redirects", "Resource Type": "Resource Type", "Resource ID": "Resource ID", "Attributes": "Attributes", @@ -93,9 +149,7 @@ "ISO-4217 3-letter currency code (e.g. USD, EUR).": "ISO-4217 3-letter currency code (e.g. USD, EUR).", "Name of the company being billed. Stored as a plain-text label on the invoice.": "Name of the company being billed. Stored as a plain-text label on the invoice.", "Select a customer.": "Select a customer.", - "Overrides the Customer dropdown. Paste an ID from a previous step.": "Overrides the Customer dropdown. Paste an ID from a previous step.", "Select a Customer User.": "Select a Customer User.", - "Overrides the Customer User dropdown.": "Overrides the Customer User dropdown.", "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.", "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.", "Total invoice amount. Should equal the sum of all line item row totals.": "Total invoice amount. Should equal the sum of all line item row totals.", @@ -108,15 +162,16 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "Seller contact / address HTML string.", "URL for the external payment page.": "URL for the external payment page.", - "Search organizations.": "Search organizations.", - "Overrides the Organization dropdown.": "Overrides the Organization dropdown.", - "Search user by name, username or email.": "Search user by name, username or email.", - "Overrides the Owner dropdown.": "Overrides the Owner dropdown.", - "Search websites.": "Search websites.", - "Overrides the Website dropdown.": "Overrides the Website dropdown.", + "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", + "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "The organization this record belongs to.": "The organization this record belongs to.", + "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", + "The website this record is associated with.": "The website this record is associated with.", "Invoice internal status (e.g. Draft, Open).": "Invoice internal status (e.g. Draft, Open).", - "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Overrides the Internal Status dropdown (e.g. draft, open, paid).", "Invoice line items. Each item is sent via JSON:API included.": "Invoice line items. Each item is sent via JSON:API included.", + "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}": "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}", + "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}": "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}", + "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}": "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}", "The customer this order belongs to.": "The customer this order belongs to.", "Unique order reference (e.g. FR1012401Z).": "Unique order reference (e.g. FR1012401Z).", "Purchase order number provided by the buyer.": "Purchase order number provided by the buyer.", @@ -128,50 +183,75 @@ "The shipping method type (e.g. \"primary\").": "The shipping method type (e.g. \"primary\").", "Prevent the promotions engine from running for this order.": "Prevent the promotions engine from running for this order.", "Order internal status (e.g. Open, Cancelled).": "Order internal status (e.g. Open, Cancelled).", - "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Overrides the Internal Status dropdown (e.g. order_internal_status.open).", "Search payment terms.": "Search payment terms.", - "Overrides the Payment Term dropdown.": "Overrides the Payment Term dropdown.", "Search warehouses.": "Search warehouses.", - "Overrides the Warehouse dropdown.": "Overrides the Warehouse dropdown.", "Search orders to use as the parent order.": "Search orders to use as the parent order.", - "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Overrides the Parent Order dropdown. Paste an order ID from a previous step.", "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).", - "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.", "Address label (e.g. \"Main Office\").": "Address label (e.g. \"Main Office\").", "ISO-3166 country. Start typing to filter the list.": "ISO-3166 country. Start typing to filter the list.", - "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).", "Region or state. Select a country first. Start typing to filter.": "Region or state. Select a country first. Start typing to filter.", - "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).", "Free-text region for countries without predefined regions.": "Free-text region for countries without predefined regions.", "Address label (e.g. \"Warehouse East\").": "Address label (e.g. \"Warehouse East\").", - "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).", - "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).", "Order line items.": "Order line items.", "Search products.": "Search products.", - "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open", - "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.", - "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", - "Request body as a JSON object (used for POST / PATCH).": "Request body as a JSON object (used for POST / PATCH).", - "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.", - "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).", - "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", - "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", - "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.", - "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).", + "A human-readable name that identifies the customer (company).": "A human-readable name that identifies the customer (company).", + "A unique identifier from an external system.": "A unique identifier from an external system.", + "Customer's value added tax identification number.": "Customer's value added tax identification number.", + "The parent company this customer (division) reports to.": "The parent company this customer (division) reports to.", + "Search customer groups.": "Search customer groups.", + "Search customer tax codes.": "Search customer tax codes.", + "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").": "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").", + "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", + "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", + "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", + "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", + "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", + "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", + "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", + "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", + "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", + "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", + "Updated email address of the customer user.": "Updated email address of the customer user.", + "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Enable or disable the storefront account.": "Enable or disable the storefront account.", + "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", + "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", + "Email address of the user.": "Email address of the user.", + "Job title or position.": "Job title or position.", + "When disabled the user cannot log in. Defaults to true.": "When disabled the user cannot log in. Defaults to true.", + "The business unit that owns this record.": "The business unit that owns this record.", + "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.": "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.", + "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.": "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.", + "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.": "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.", + "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.": "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.", + "Authentication status of the user (e.g. active, reset, locked).": "Authentication status of the user (e.g. active, reset, locked).", + "The numeric ID of the user to update.": "The numeric ID of the user to update.", + "Updated login name for the user.": "Updated login name for the user.", + "Updated email address of the user.": "Updated email address of the user.", + "Enable or disable the user account.": "Enable or disable the user account.", + "Search business units.": "Search business units.", + "Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.", + "Enable for files like PDFs, images, etc.": "Enable for files like PDFs, images, etc.", + "e.g. orders, invoices, products. Taken from _type when left empty.": "e.g. orders, invoices, products. Taken from _type when left empty.", + "Leave empty to create a record, or when the input carries an id.": "Leave empty to create a record, or when the input carries an id.", + "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.": "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.", + "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", + "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", + "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", "GET": "GET", "POST": "POST", "PATCH": "PATCH", + "PUT": "PUT", "DELETE": "DELETE", - "New Order": "New Order", - "Order Removal": "Order Removal", - "Order Update": "Order Update", - "New Invoice": "New Invoice", - "Invoice Removal": "Invoice Removal", - "Invoice Update": "Invoice Update", - "Triggered when a new order is created": "Triggered when a new order is created", - "Triggered when an order is deleted": "Triggered when an order is deleted", - "Triggered when an order is updated": "Triggered when an order is updated", - "Triggered when a new invoice is created": "Triggered when a new invoice is created", - "Triggered when an invoice is deleted": "Triggered when an invoice is deleted", - "Triggered when an invoice is updated": "Triggered when an invoice is updated" + "HEAD": "HEAD", + "None": "None", + "JSON": "JSON", + "Form Data": "Form Data", + "Raw": "Raw", + "Oro Webhook Event": "Oro Webhook Event", + "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", + "Topic": "Topic", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" } diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json index 0aeace870c2f..5f5dff15e675 100644 --- a/packages/pieces/community/orocommerce/src/i18n/uk.json +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -4,21 +4,37 @@ "Admin Prefix": "Префікс адмін-панелі", "Client ID": "Ідентифікатор клієнта (Client ID)", "Client Secret": "Секрет клієнта (Client Secret)", + "Default HTTP Headers": "Default HTTP Headers", + "Internal infrastructure": "Internal infrastructure", "The base URL of your OroCommerce instance (e.g., https://your-store.com).": "Базова URL-адреса вашого інстансу OroCommerce (наприклад, https://your-store.com).", "The admin panel URL prefix (default is \"admin\").": "Префікс URL адмін-панелі (за замовчуванням \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "OAuth Client ID з вашого OAuth-застосунку OroCommerce.", "The OAuth Client Secret from your OroCommerce OAuth application.": "OAuth Client Secret з вашого OAuth-застосунку OroCommerce.", + "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nАвтентифікуйтеся в API OroCommerce за допомогою OAuth 2.0 Client Credentials.\n\n**Кроки для отримання облікових даних:**\n1. Увійдіть в адмін-панель OroCommerce.\n2. Перейдіть до **System** > **User Management** > **OAuth Applications**.\n3. Натисніть **Create OAuth Application** та налаштуйте:\n - **Application Name**: Вкажіть описову назву (наприклад, \"Activepieces Integration\")\n - **Grants**: Оберіть **Client Credentials**\n - **Redirect URIs**: Не потрібно для потоку Client Credentials\n4. Збережіть застосунок і скопіюйте **Client ID** та **Client Secret**.\n5. Запам’ятайте ваш **Server URL** (наприклад, `https://your-store.com`) та **Admin Prefix** (зазвичай `admin`).\n ", "Create Invoice": "Створити рахунок-фактуру", "Create Order": "Створити замовлення", - "API Call": "API-запит", + "Create Customer": "Create Customer", + "Update Customer": "Update Customer", + "Create Customer User": "Create Customer User", + "Update Customer User": "Update Customer User", + "Create User": "Create User", + "Update User": "Update User", + "Custom API Call": "Custom API Call", "Serialize JSON:API Request": "Серіалізувати запит JSON:API", "Unserialize JSON:API Response": "Десеріалізувати відповідь JSON:API", "Creates a new invoice record in OroCommerce.": "Створює новий запис рахунку-фактури в OroCommerce.", "Creates a new order record in OroCommerce.": "Створює новий запис замовлення в OroCommerce.", + "Creates a new customer (company) record in OroCommerce.": "Creates a new customer (company) record in OroCommerce.", + "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.": "Updates an existing customer (company) record in OroCommerce. Only provided fields are changed.", + "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", + "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", + "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Виконує прямий автентифікований виклик до OroCommerce JSON:API.", - "Converts a plain object into a JSON:API-compliant request body. Relationship fields are detected automatically - any array or object value is treated as a relationship, only scalars become attributes. The result can be passed directly as the Request Body of the API Call action.": "Перетворює простий об’єкт у тіло запиту, сумісне з JSON:API. Поля зв’язків визначаються автоматично — будь-яке значення типу масив або об’єкт розглядається як relationship, а лише скалярні значення стають attributes. Результат можна напряму передати в поле Request Body дії API Call.", - "Flattens a JSON:API response body into a plain object (or array of objects). Included relationships are resolved and inlined. Unresolved relationship references are kept as { _type, id } stubs so the document can be re-serialized back to a valid JSON:API request without data loss. Pass the \"body\" output of the API Call action as the Response input.": "Перетворює тіло відповіді JSON:API у простий об’єкт (або масив об’єктів). Зв’язки з included розв’язуються та вбудовуються. Нерозв’язані посилання на зв’язки зберігаються як заглушки { _type, id }, щоб документ можна було повторно серіалізувати у коректний запит JSON:API без втрати даних. Передайте вихід \"body\" з дії API Call у вхід Response.", + "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", + "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", "Invoice Date": "Дата рахунку", "Currency": "Валюта", "Customer Name": "Назва клієнта", @@ -36,11 +52,16 @@ "Shipping Method": "Спосіб доставки", "Seller Info": "Інформація про продавця", "External Payment URL": "Зовнішній URL оплати", + "Invoice PDF": "Invoice PDF", + "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Організація", - "User": "Користувач", + "Owner": "Owner", "Website": "Вебсайт", "Internal Status": "Внутрішній статус", "Line Items": "Позиції", + "Additional Attributes": "Additional Attributes", + "Additional Relations": "Additional Relations", + "Additional Headers": "Additional Headers", "Identifier": "Ідентифікатор", "PO Number": "Номер PO", "Customer Notes": "Примітки клієнта", @@ -78,11 +99,46 @@ "Shipping: Region / State": "Адреса доставки: регіон / штат", "Shipping: Custom Region": "Адреса доставки: інший регіон", "Product": "Товар", + "Name": "Name", + "External ID": "External ID", + "VAT ID": "VAT ID", + "Parent Customer": "Parent Customer", + "Customer Group": "Customer Group", + "Tax Code": "Tax Code", + "Internal Rating": "Internal Rating", + "Addresses": "Addresses", + "Customer ID": "Customer ID", + "Email": "Email", + "Password": "Password", + "Name Prefix": "Name Prefix", + "First Name": "First Name", + "Middle Name": "Middle Name", + "Last Name": "Last Name", + "Name Suffix": "Name Suffix", + "Enabled": "Enabled", + "Confirmed": "Confirmed", + "Birthday": "Birthday", + "Roles (replaces all existing roles)": "Roles (replaces all existing roles)", + "Customer User ID": "Customer User ID", + "Username": "Username", + "Phone": "Phone", + "Owner (Business Unit)": "Owner (Business Unit)", + "Business Units (replaces all existing business units)": "Business Units (replaces all existing business units)", + "User Roles (replaces all existing roles)": "User Roles (replaces all existing roles)", + "Organizations (replaces all existing organizations)": "Organizations (replaces all existing organizations)", + "User Groups (replaces all existing groups)": "User Groups (replaces all existing groups)", + "Auth Status": "Auth Status", + "User ID": "User ID", + "Business Unit": "Business Unit", "Method": "Метод", - "Resource URI": "URI ресурсу", "Headers": "Заголовки", "Query Parameters": "Параметри запиту", - "Request Body": "Тіло запиту", + "Body Type": "Body Type", + "Body": "Body", + "Response is Binary ?": "Response is Binary ?", + "No Error on Failure": "No Error on Failure", + "Timeout (in seconds)": "Timeout (in seconds)", + "Follow redirects": "Follow redirects", "Resource Type": "Тип ресурсу", "Resource ID": "ID ресурсу", "Attributes": "Атрибути", @@ -93,9 +149,7 @@ "ISO-4217 3-letter currency code (e.g. USD, EUR).": "3-літерний код валюти ISO-4217 (наприклад, USD, EUR).", "Name of the company being billed. Stored as a plain-text label on the invoice.": "Назва компанії, якій виставляється рахунок. Зберігається як звичайна текстова мітка в рахунку.", "Select a customer.": "Оберіть клієнта.", - "Overrides the Customer dropdown. Paste an ID from a previous step.": "Перевизначає випадаючий список Customer. Вставте ID з попереднього кроку.", "Select a Customer User.": "Оберіть користувача клієнта.", - "Overrides the Customer User dropdown.": "Перевизначає випадаючий список Customer User.", "An optional ID reference to a customer. Can be used for storing an arbitrary external ID.": "Необов’язкове посилання-ID на клієнта. Можна використати для збереження довільного зовнішнього ID.", "An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.": "Необов’язкове посилання-ID на користувача клієнта. Можна використати для збереження довільного зовнішнього ID.", "Total invoice amount. Should equal the sum of all line item row totals.": "Загальна сума рахунку. Має дорівнювати сумі всіх підсумків по позиціях.", @@ -108,15 +162,16 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Мітка способу доставки (наприклад, \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "HTML-рядок контактів / адреси продавця.", "URL for the external payment page.": "URL зовнішньої сторінки оплати.", - "Search organizations.": "Пошук організацій.", - "Overrides the Organization dropdown.": "Перевизначає випадаючий список Organization.", - "Search user by name, username or email.": "Пошук користувача за ім’ям, ім’ям користувача або email.", - "Overrides the Owner dropdown.": "Перевизначає випадаючий список Owner.", - "Search websites.": "Пошук вебсайтів.", - "Overrides the Website dropdown.": "Перевизначає випадаючий список Website.", + "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", + "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "The organization this record belongs to.": "The organization this record belongs to.", + "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", + "The website this record is associated with.": "The website this record is associated with.", "Invoice internal status (e.g. Draft, Open).": "Внутрішній статус рахунку (наприклад, Draft, Open).", - "Overrides the Internal Status dropdown (e.g. draft, open, paid).": "Перевизначає випадаючий список Internal Status (наприклад, draft, open, paid).", "Invoice line items. Each item is sent via JSON:API included.": "Позиції рахунку. Кожна позиція надсилається через JSON:API included.", + "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}": "Custom fields, merged after the standard ones. Example: {\"myField\": \"value\"}", + "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}": "Custom relationships in linkage format. Example: {\"myRelation\": {\"data\": {\"type\": \"myentities\", \"id\": \"1\"}}}", + "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}": "Headers for this step; override the connection defaults. Example: {\"X-Include\": \"totalCount\"}", "The customer this order belongs to.": "Клієнт, якому належить це замовлення.", "Unique order reference (e.g. FR1012401Z).": "Унікальний ідентифікатор замовлення (наприклад, FR1012401Z).", "Purchase order number provided by the buyer.": "Номер замовлення на закупівлю (PO), наданий покупцем.", @@ -128,50 +183,75 @@ "The shipping method type (e.g. \"primary\").": "Тип способу доставки (наприклад, \"primary\").", "Prevent the promotions engine from running for this order.": "Запобігти запуску механізму промоакцій для цього замовлення.", "Order internal status (e.g. Open, Cancelled).": "Внутрішній статус замовлення (наприклад, Open, Cancelled).", - "Overrides the Internal Status dropdown (e.g. order_internal_status.open).": "Перевизначає випадаючий список Internal Status (наприклад, order_internal_status.open).", "Search payment terms.": "Пошук умов оплати.", - "Overrides the Payment Term dropdown.": "Перевизначає випадаючий список Payment Term.", "Search warehouses.": "Пошук складів.", - "Overrides the Warehouse dropdown.": "Перевизначає випадаючий список Warehouse.", "Search orders to use as the parent order.": "Пошук замовлень для використання як батьківського замовлення.", - "Overrides the Parent Order dropdown. Paste an order ID from a previous step.": "Перевизначає випадаючий список Parent Order. Вставте ID замовлення з попереднього кроку.", "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Статус замовлення, яким керує зовнішня система (актуально лише коли ввімкнено \"Enable External Status Management\").", - "Overrides the Status dropdown. Only applied when \"Enable External Status Management\" is on.": "Перевизначає випадаючий список Status. Застосовується лише коли ввімкнено \"Enable External Status Management\".", "Address label (e.g. \"Main Office\").": "Мітка адреси (наприклад, \"Main Office\").", "ISO-3166 country. Start typing to filter the list.": "Країна за ISO-3166. Почніть вводити, щоб відфільтрувати список.", - "Overrides the Billing Country dropdown (ISO-3166 2-letter code, e.g. US).": "Перевизначає випадаючий список Billing Country (2-літерний код ISO-3166, наприклад US).", "Region or state. Select a country first. Start typing to filter.": "Регіон або штат. Спочатку оберіть країну. Почніть вводити, щоб відфільтрувати.", - "Overrides the Billing Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Перевизначає випадаючий список Billing Region (код ISO 3166-2, наприклад US-NY).", "Free-text region for countries without predefined regions.": "Регіон у довільному форматі для країн без попередньо визначених регіонів.", "Address label (e.g. \"Warehouse East\").": "Мітка адреси (наприклад, \"Warehouse East\").", - "Overrides the Shipping Country dropdown (ISO-3166 2-letter code, e.g. US).": "Перевизначає випадаючий список Shipping Country (2-літерний код ISO-3166, наприклад US).", - "Overrides the Shipping Region dropdown (ISO 3166-2 code, e.g. US-NY).": "Перевизначає випадаючий список Shipping Region (код ISO 3166-2, наприклад US-NY).", "Order line items.": "Позиції замовлення.", "Search products.": "Пошук товарів.", - "Path relative to the API base, e.g. /orders, /orders/42, /invoices?filter[status]=open": "Шлях відносно бази API, наприклад /orders, /orders/42, /invoices?filter[status]=open", - "Request headers as a JSON object. Defaults include Accept and X-Include for JSON:API.": "Заголовки запиту у вигляді JSON-об’єкта. Типові значення включають Accept та X-Include для JSON:API.", - "Additional query parameters as a JSON object, e.g. {\"filter[status]\":\"open\",\"page[size]\":\"20\"}": "Додаткові параметри запиту у вигляді JSON-об’єкта, наприклад {\"filter[status]\":\"open\",\"page[size]\":\"20\"}", - "Request body as a JSON object (used for POST / PATCH).": "Тіло запиту у вигляді JSON-об’єкта (використовується для POST / PATCH).", - "The JSON:API resource type (e.g. orders, invoices, products). The _type resolved as resource type automatically.": "Тип ресурсу JSON:API (наприклад, orders, invoices, products). Поле _type автоматично визначається як тип ресурсу.", - "ID of the resource - leave empty when creating a new record (POST) or when the input includes id field (the id field is picked up automatically).": "ID ресурсу — залиште порожнім під час створення нового запису (POST) або коли вхідні дані містять поле id (це поле буде зчитано автоматично).", - "The flat object to serialize. Pass the full JSON document here - arrays and objects are automatically placed into \"relationships\"; only scalar values become attributes. For manual construction use plain key/value pairs, e.g. {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.": "Плоский об’єкт для серіалізації. Передайте сюди повний JSON-документ — масиви та об’єкти автоматично потрапляють у \"relationships\"; лише скалярні значення стають attributes. Для ручного складання використовуйте прості пари ключ/значення, наприклад {\"currency\":\"USD\",\"poNumber\":\"PO-001\"}.", - "Optional explicit relationship map that takes priority over any relationships detected inside Attributes. Each key is a relationship name; the value is a single linkage object {\"type\":\"customers\",\"id\":\"42\"}, an array of linkage objects, or null. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}": "Необов’язкова явна мапа relationships, яка має пріоритет над будь-якими зв’язками, визначеними всередині Attributes. Кожен ключ — назва relationship; значення — один об’єкт зв’язку {\"type\":\"customers\",\"id\":\"42\"}, масив таких об’єктів або null. Приклад: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"},\"lineItems\":[{\"type\":\"orderlineitems\",\"id\":\"1\"}]}", - "Optional array of full JSON:API resource objects to embed in the \"included\" section of the request. If the input to Attributes already contains an \"included\" array (e.g. the raw API Call response body) it will be forwarded automatically - this field only needs to be set when adding or overriding included resources manually. Each entry must be a valid JSON:API resource with \"type\", \"id\", and \"attributes\" fields.": "Необов’язковий масив повних ресурсів JSON:API для вбудовування в розділ \"included\" запиту. Якщо вхідні дані Attributes вже містять масив \"included\" (наприклад, сире тіло відповіді API Call), він буде переданий автоматично — це поле потрібно лише для ручного додавання або перевизначення included-ресурсів. Кожен елемент має бути коректним ресурсом JSON:API з полями \"type\", \"id\" та \"attributes\".", - "The raw JSON:API response body - the \"body\" field from the API Call action output. Must contain a top-level \"data\" key (single resource or collection).": "Сире тіло відповіді JSON:API — поле \"body\" з виходу дії API Call. Має містити ключ верхнього рівня \"data\" (один ресурс або колекція).", + "A human-readable name that identifies the customer (company).": "A human-readable name that identifies the customer (company).", + "A unique identifier from an external system.": "A unique identifier from an external system.", + "Customer's value added tax identification number.": "Customer's value added tax identification number.", + "The parent company this customer (division) reports to.": "The parent company this customer (division) reports to.", + "Search customer groups.": "Search customer groups.", + "Search customer tax codes.": "Search customer tax codes.", + "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").": "Internal customer rating (e.g. \"1 of 5\", \"5 of 5\").", + "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", + "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", + "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", + "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", + "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", + "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", + "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", + "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", + "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", + "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", + "Updated email address of the customer user.": "Updated email address of the customer user.", + "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Enable or disable the storefront account.": "Enable or disable the storefront account.", + "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", + "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", + "Email address of the user.": "Email address of the user.", + "Job title or position.": "Job title or position.", + "When disabled the user cannot log in. Defaults to true.": "When disabled the user cannot log in. Defaults to true.", + "The business unit that owns this record.": "The business unit that owns this record.", + "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.": "The complete set of business units the user belongs to. Saving replaces the existing list, so include every business unit the user should keep.", + "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.": "The complete set of back-office roles for the user. Saving replaces the existing roles, so include every role the user should keep.", + "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.": "The complete set of organizations the user has access to. Saving replaces the existing list, so include every organization the user should keep.", + "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.": "The complete set of back-office groups for the user. Saving replaces the existing groups, so include every group the user should keep.", + "Authentication status of the user (e.g. active, reset, locked).": "Authentication status of the user (e.g. active, reset, locked).", + "The numeric ID of the user to update.": "The numeric ID of the user to update.", + "Updated login name for the user.": "Updated login name for the user.", + "Updated email address of the user.": "Updated email address of the user.", + "Enable or disable the user account.": "Enable or disable the user account.", + "Search business units.": "Search business units.", + "Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.", + "Enable for files like PDFs, images, etc.": "Enable for files like PDFs, images, etc.", + "e.g. orders, invoices, products. Taken from _type when left empty.": "e.g. orders, invoices, products. Taken from _type when left empty.", + "Leave empty to create a record, or when the input carries an id.": "Leave empty to create a record, or when the input carries an id.", + "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.": "A flat object, Unserialize output, or a single-resource JSON:API document. Values marked with _type or shaped like {\"type\",\"id\"} become relationships.", + "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", + "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", + "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", "GET": "GET", "POST": "POST", "PATCH": "PATCH", + "PUT": "PUT", "DELETE": "DELETE", - "New Order": "Нове замовлення", - "Order Removal": "Видалення замовлення", - "Order Update": "Оновлення замовлення", - "New Invoice": "Новий рахунок-фактура", - "Invoice Removal": "Видалення рахунку-фактури", - "Invoice Update": "Оновлення рахунку-фактури", - "Triggered when a new order is created": "Спрацьовує, коли створюється нове замовлення", - "Triggered when an order is deleted": "Спрацьовує, коли замовлення видалено", - "Triggered when an order is updated": "Спрацьовує, коли замовлення оновлено", - "Triggered when a new invoice is created": "Спрацьовує, коли створюється новий рахунок-фактура", - "Triggered when an invoice is deleted": "Спрацьовує, коли рахунок-фактуру видалено", - "Triggered when an invoice is updated": "Спрацьовує, коли рахунок-фактуру оновлено" + "HEAD": "HEAD", + "None": "None", + "JSON": "JSON", + "Form Data": "Form Data", + "Raw": "Raw", + "Oro Webhook Event": "Oro Webhook Event", + "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", + "Topic": "Topic", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" } diff --git a/packages/pieces/community/orocommerce/tools/check-i18n.mjs b/packages/pieces/community/orocommerce/tools/check-i18n.mjs new file mode 100644 index 000000000000..9d9d706a4bda --- /dev/null +++ b/packages/pieces/community/orocommerce/tools/check-i18n.mjs @@ -0,0 +1,259 @@ +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const SOURCE_FILE = 'translation.json'; +const MAX_KEY_LENGTH_FOR_CORWDIN = 512; +const RUNTIME_LOCALES = new Set([ + 'nl', + 'en', + 'de', + 'fr', + 'es', + 'ja', + 'zh', + 'pt', + 'ar', + 'zh-TW', +]); + +const PATHS_TO_VALUES_TO_TRANSLATE = [ + 'description', + 'auth.username.displayName', + 'auth.username.description', + 'auth.password.displayName', + 'auth.password.description', + 'auth.props.*.displayName', + 'auth.props.*.description', + 'auth.props.*.options.options.*.label', + 'auth.description', + 'actions.*.displayName', + 'actions.*.description', + 'actions.*.props.*.displayName', + 'actions.*.props.*.description', + 'actions.*.props.*.options.options.*.label', + 'triggers.*.displayName', + 'triggers.*.description', + 'triggers.*.props.*.displayName', + 'triggers.*.props.*.description', + 'triggers.*.props.*.options.options.*.label', +]; + +function getPropertyValue(object, path) { + const parsedKeys = path.split('.'); + if (parsedKeys[0] === '*') { + return Object.values(object ?? {}) + .map((item) => getPropertyValue(item, parsedKeys.slice(1).join('.'))) + .filter(Boolean) + .flat(); + } + const nextObject = (object ?? {})[parsedKeys[0]]; + if (nextObject && parsedKeys.length > 1) { + return getPropertyValue(nextObject, parsedKeys.slice(1).join('.')); + } + return nextObject; +} + +function collectTranslatableStrings({ piece }) { + const translation = {}; + for (const path of PATHS_TO_VALUES_TO_TRANSLATE) { + const value = getPropertyValue(piece, path); + if (!value) { + continue; + } + if (typeof value === 'string') { + translation[value.slice(0, MAX_KEY_LENGTH_FOR_CORWDIN)] = value; + } else if (Array.isArray(value)) { + for (const item of value) { + if (typeof item === 'string') { + translation[item.slice(0, MAX_KEY_LENGTH_FOR_CORWDIN)] = item; + } + } + } + } + return translation; +} + +async function loadPiece({ modulePath }) { + if (!existsSync(modulePath)) { + throw new Error( + `Built piece not found at ${modulePath}. Run "npm run build" (or pass --bundle=) before checking i18n.` + ); + } + const module = await import(pathToFileURL(modulePath).href); + for (const exported of Object.values(module)) { + if ( + exported !== null && + exported !== undefined && + exported.constructor?.name === 'Piece' + ) { + return { + description: exported.description, + auth: exported.auth, + actions: exported._actions, + triggers: exported._triggers, + }; + } + } + throw new Error(`No exported Piece found in ${modulePath}.`); +} + +async function readJson({ filePath }) { + const content = await readFile(filePath, 'utf8'); + const parsed = JSON.parse(content); + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error(`${filePath} must contain a JSON object.`); + } + return parsed; +} + +async function writeJson({ filePath, value }) { + await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +async function listLocaleFiles({ i18nDir }) { + const entries = await readdir(i18nDir); + return entries + .filter((entry) => entry.endsWith('.json') && entry !== SOURCE_FILE) + .sort(); +} + +function difference({ from, without }) { + return [...from].filter((key) => !without.has(key)); +} + +function reportList({ label, keys, limit = 10 }) { + const shown = keys.slice(0, limit); + const suffix = keys.length > limit ? ` (+${keys.length - limit} more)` : ''; + return `${label} (${keys.length}):\n${shown.map((key) => ` - ${key}`).join('\n')}${suffix}`; +} + +async function run() { + const args = process.argv.slice(2); + const option = (name, fallback) => { + const hit = args.find((arg) => arg.startsWith(`--${name}=`)); + return hit ? hit.slice(name.length + 3) : fallback; + }; + const write = args.includes('--write'); + const strictUntranslated = args.includes('--strict-untranslated'); + const modulePath = resolve( + option('bundle', join(PACKAGE_ROOT, 'dist', 'src', 'index.js')) + ); + const i18nDir = resolve(option('i18n-dir', join(PACKAGE_ROOT, 'src', 'i18n'))); + + const piece = await loadPiece({ modulePath }); + const expected = collectTranslatableStrings({ piece }); + const expectedKeys = new Set(Object.keys(expected)); + + const errors = []; + const warnings = []; + + const sourcePath = join(i18nDir, SOURCE_FILE); + if (write) { + await writeJson({ filePath: sourcePath, value: expected }); + console.log(`wrote ${expectedKeys.size} keys to src/i18n/${SOURCE_FILE}`); + } + + const source = await readJson({ filePath: sourcePath }); + const sourceKeys = new Set(Object.keys(source)); + + const missingFromSource = difference({ from: expectedKeys, without: sourceKeys }); + const staleInSource = difference({ from: sourceKeys, without: expectedKeys }); + + if (missingFromSource.length > 0) { + errors.push( + `src/i18n/${SOURCE_FILE} is missing keys the piece exposes. ${reportList({ label: 'Missing', keys: missingFromSource })}` + ); + } + if (staleInSource.length > 0) { + errors.push( + `src/i18n/${SOURCE_FILE} has keys the piece no longer exposes. ${reportList({ label: 'Stale', keys: staleInSource })}` + ); + } + for (const [key, value] of Object.entries(source)) { + if (typeof value !== 'string' || value.trim().length === 0) { + errors.push(`src/i18n/${SOURCE_FILE} has an empty value for "${key}".`); + } + } + + for (const file of await listLocaleFiles({ i18nDir })) { + const locale = file.replace(/\.json$/, ''); + const localePath = join(i18nDir, file); + const existing = await readJson({ filePath: localePath }); + + if (!RUNTIME_LOCALES.has(locale)) { + warnings.push( + `src/i18n/${file} is never loaded: "${locale}" is not one of the locales Activepieces supports (${[...RUNTIME_LOCALES].join(', ')}).` + ); + } + + if (write) { + const reconciled = {}; + for (const key of expectedKeys) { + reconciled[key] = existing[key] ?? expected[key]; + } + await writeJson({ filePath: localePath, value: reconciled }); + const added = difference({ from: expectedKeys, without: new Set(Object.keys(existing)) }).length; + const removed = difference({ from: new Set(Object.keys(existing)), without: expectedKeys }).length; + console.log(`reconciled src/i18n/${file}: +${added} seeded, -${removed} stale`); + continue; + } + + const localeKeys = new Set(Object.keys(existing)); + const missing = difference({ from: expectedKeys, without: localeKeys }); + const stale = difference({ from: localeKeys, without: expectedKeys }); + + if (missing.length > 0) { + errors.push( + `src/i18n/${file} is missing keys present in ${SOURCE_FILE}. ${reportList({ label: 'Missing', keys: missing })}` + ); + } + if (stale.length > 0) { + errors.push( + `src/i18n/${file} has keys absent from ${SOURCE_FILE}. ${reportList({ label: 'Stale', keys: stale })}` + ); + } + + const untranslated = []; + for (const [key, value] of Object.entries(existing)) { + if (typeof value !== 'string' || value.trim().length === 0) { + errors.push(`src/i18n/${file} has an empty value for "${key}".`); + } else if (expectedKeys.has(key) && value === expected[key]) { + untranslated.push(key); + } + } + if (untranslated.length > 0) { + const message = `src/i18n/${file} repeats the English source verbatim. ${reportList({ label: 'Untranslated', keys: untranslated })}`; + if (strictUntranslated) { + errors.push(message); + } else { + warnings.push(message); + } + } + } + + for (const warning of warnings) { + console.warn(`warning: ${warning}`); + } + + if (errors.length > 0) { + for (const error of errors) { + console.error(`error: ${error}`); + } + console.error( + `\ni18n check failed with ${errors.length} error(s). Regenerate with "npm run cli pieces generate-translation-file orocommerce" or "npm run i18n:write", then translate the seeded keys.` + ); + process.exit(1); + } + + console.log( + `i18n check passed: ${expectedKeys.size} keys across ${SOURCE_FILE} and ${(await listLocaleFiles({ i18nDir })).length} locale file(s).` + ); +} + +run().catch((error) => { + console.error(`error: ${error.message}`); + process.exit(1); +}); diff --git a/turbo.json b/turbo.json index 44111d9b5775..8648366c5cbf 100644 --- a/turbo.json +++ b/turbo.json @@ -113,6 +113,11 @@ "inputs": ["src/**", "test/**", "tsconfig*.json", "vitest.config.*"], "outputs": ["coverage/**"] }, + "i18n:check": { + "dependsOn": ["build"], + "inputs": ["src/**", "tools/**", "package.json"], + "outputs": [] + }, "test-ce": { "dependsOn": ["build"], "cache": false From e768ce19b6a75c7733b1ab56e05825c8b1bb7145 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 13 Aug 2026 16:49:32 +0200 Subject: [PATCH 35/50] chore(orocommerce): make the package metadata honest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit minimumSupportedRelease claimed 0.36.1. The package declares workspace dependencies on @activepieces/core-utils and @activepieces/core-piece-types and follows the bundle model, none of which exist before 0.86.0 — an 0.36.1 server cannot load this piece at all. The floor is now the real one. bun.lock had no entry for this workspace, so a clean checkout resolved it by accident rather than by the lockfile. The logo URL keeps its host but escapes the parentheses in O(logo).svg, which several markdown and URL parsers treat as delimiters. Renaming the asset needs someone with write access to static.oroinc.com; the encoded form is verified to serve the same image. package.json gains description, keywords, license, homepage and repository. The piece sits under packages/pieces/community, so the repository's root MIT terms apply; no separate LICENSE file is added, since no other piece ships one and the only thing that would need it is standalone npm publishing, which is not settled. The README leads with what a user needs — connection setup, the action and trigger list, where to file issues — and keeps the existing engineering notes below for contributors, with new sections on the i18n gate and on why the password props cannot currently be secrets. Its claim that the repository "bans code comments" is softened to match the seventy-odd section markers actually in the source, and the claim that CI does not run these tests is now false and removed. Also drops plan-oroCommerceActions.prompt.md_, a leftover planning artifact, and restores .nvmrc to its upstream byte-for-byte form. Co-Authored-By: Claude Opus 5 (1M context) --- .nvmrc | 2 +- bun.lock | 16 + .../pieces/community/orocommerce/README.md | 142 +++- .../pieces/community/orocommerce/package.json | 16 + .../plan-oroCommerceActions.prompt.md_ | 784 ------------------ .../pieces/community/orocommerce/src/index.ts | 4 +- 6 files changed, 164 insertions(+), 800 deletions(-) delete mode 100644 packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ diff --git a/.nvmrc b/.nvmrc index a4a7a41bca7c..f9c0a7587323 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.14.0 +v24.14.0 \ No newline at end of file diff --git a/bun.lock b/bun.lock index 0136b535da1a..ab682a6355cb 100644 --- a/bun.lock +++ b/bun.lock @@ -6567,6 +6567,20 @@ "tslib": "^2.3.0", }, }, + "packages/pieces/community/orocommerce": { + "name": "@activepieces/piece-orocommerce", + "version": "0.2.0", + "dependencies": { + "@activepieces/core-piece-types": "workspace:*", + "@activepieces/core-utils": "workspace:*", + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + }, + "devDependencies": { + "tslib": "2.6.2", + "vitest": "3.2.6", + }, + }, "packages/pieces/community/outseta": { "name": "@activepieces/piece-outseta", "version": "0.2.3", @@ -11967,6 +11981,8 @@ "@activepieces/piece-orimon": ["@activepieces/piece-orimon@workspace:packages/pieces/community/orimon"], + "@activepieces/piece-orocommerce": ["@activepieces/piece-orocommerce@workspace:packages/pieces/community/orocommerce"], + "@activepieces/piece-outseta": ["@activepieces/piece-outseta@workspace:packages/pieces/community/outseta"], "@activepieces/piece-paddle": ["@activepieces/piece-paddle@workspace:packages/pieces/community/paddle"], diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 2d5db4af285a..06c98859c7b2 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -1,9 +1,68 @@ # OroCommerce piece -Actions and a webhook trigger for the OroCommerce back-office JSON:API. +Automate [OroCommerce](https://oroinc.com/orocommerce/) from Activepieces: create and update +customers, storefront and back-office users, orders and invoices, and start flows from OroCommerce +webhook events. Everything runs against the OroCommerce back-office JSON:API. -This repo bans code comments, so the non-obvious rules of this piece live here. Read the sections -below before changing anything under `src/` — most of them exist because of a bug that is easy to +## Setting up a connection + +The piece authenticates with **OAuth 2.0 Client Credentials**. Create the credentials in Oro first: + +1. Log in to your OroCommerce admin panel. +2. Go to **System → User Management → OAuth Applications**. +3. Click **Create OAuth Application**: + - **Application Name** — anything descriptive, e.g. `Activepieces Integration`. + - **Grants** — select **Client Credentials**. + - **Redirect URIs** — leave empty, the Client Credentials flow does not use them. +4. Save, then copy the **Client ID** and **Client Secret**. + +Then add the connection in Activepieces: + +| Field | Value | +| --- | --- | +| **Server URL** | Base URL of your instance, e.g. `https://your-store.com` | +| **Admin Prefix** | Admin panel prefix, usually `admin` | +| **Client ID** / **Client Secret** | From the OAuth application above | +| **Default HTTP Headers** | Optional JSON object sent with every request of this piece | +| **Internal infrastructure** | Leave off unless you run Oro's own hosted infrastructure | + +Activepieces verifies the connection with `GET regions/US-CA`. If the OAuth application's user +cannot read `regions`, the connection is reported invalid even when the credentials are correct — +grant that permission or the check will keep failing. + +## Actions + +| Action | What it does | +| --- | --- | +| **Create Customer** / **Update Customer** | The customer (company) record | +| **Create Customer User** / **Update Customer User** | Storefront accounts, with addresses | +| **Create User** / **Update User** | Back-office users, roles, groups and business units | +| **Create Order** | An order with line items and billing/shipping addresses | +| **Create Invoice** | An invoice with line items and an optional PDF attachment | +| **Custom API Call** | Any other OroCommerce JSON:API endpoint | +| **Serialize JSON:API Request** / **Unserialize JSON:API Response** | Convert between a flat object and a JSON:API document | + +Update actions change only the fields you fill in, and refuse to run when nothing is filled in +rather than sending an empty request that reports success. Note that a JSON:API `PATCH` of a +to-many relationship is a **full replace** — see *Update actions replace to-many relationships*. + +## Trigger + +**Oro Webhook Event** — starts a flow when the selected OroCommerce webhook topic fires. The topic +dropdown lists only the topics your connection can read. Enabling the trigger registers the webhook +in Oro; disabling it removes the registration. + +## Reporting issues + +Open an issue at with your OroCommerce version, the +action or trigger involved, and the error text from the run log. Do not paste client secrets, +tokens or customer data. + +--- + +The rest of this file is for contributors. The piece follows the repository convention of keeping +explanatory prose out of the source, so the non-obvious rules live here — read the relevant section +before changing anything under `src/`. Most of them exist because of a bug that is easy to reintroduce. ## How it talks to Oro @@ -179,20 +238,66 @@ those containers unconditionally without emitting empty ones on the wire. ## Local development ```bash -npx turbo run test --filter=@activepieces/piece-orocommerce # vitest (builds first) -npx turbo run lint --filter=@activepieces/piece-orocommerce -npx turbo run build --filter=@activepieces/piece-orocommerce # tsc -p tsconfig.lib.json, also the type-check -npm run lint-dev # repo-wide lint with auto-fix +npx turbo run test --filter=@activepieces/piece-orocommerce # vitest (builds first) +npx turbo run i18n:check --filter=@activepieces/piece-orocommerce # i18n gate (builds first) +npx turbo run lint --filter=@activepieces/piece-orocommerce +npx turbo run build --filter=@activepieces/piece-orocommerce # tsc -p tsconfig.lib.json, also the type-check +npm run lint-dev # repo-wide lint with auto-fix ``` There is no `typecheck` script in this package, so the root `typecheck` task is a no-op here — the build is the type-check. -**CI does not run these tests.** `.github/workflows/ci.yml` builds pieces touched by the diff, but -its test step is a hardcoded filter list (`@activepieces/engine`, `@activepieces/shared`, -`@activepieces/ai-providers`) that does not include this piece. `test/jsonapi-roundtrip.test.ts` -guards the serialize/deserialize contract above and is local-only today — run it yourself before -touching `src/lib/common/jsonapi/`. +`.github/workflows/ci.yml` runs `test` and `i18n:check` for this piece by name, alongside the other +packages in its hardcoded filter list. `test/jsonapi-roundtrip.test.ts` guards the +serialize/deserialize contract above and `test/line-items.test.ts` guards line-item validation. + +## The i18n gate + +`src/i18n/translation.json` is the English source; the per-locale files beside it are its +translations. Both are generated, not hand-maintained: + +```bash +npm run cli pieces generate-translation-file orocommerce # canonical, inside the monorepo +npm run i18n:write # same thing without the monorepo CLI +``` + +`npm run i18n:check` (`tools/check-i18n.mjs`) fails the build when they drift. It imports the +**built** piece from `dist/`, walks the same 19 metadata paths as +`pieceTranslation.pathsToValuesToTranslate` in `packages/pieces/framework/src/lib/i18n.ts`, and +truncates keys at 512 characters exactly as the official generator does. It fails on keys missing +from or stale in `translation.json`, on any locale file whose key set differs from it, and on empty +values. Values identical to the English source are a warning; `--strict-untranslated` promotes them +to errors. + +`npm run i18n:write` regenerates `translation.json` and reconciles every locale file against it — +stale keys are dropped, missing keys are seeded with the English text, and existing translations are +left untouched. Seeded keys still need translating. + +## Passwords are step inputs, and step inputs are not secrets + +Four actions take a password: `create-user`, `update-user`, `create-customer-user` and +`update-customer-user`. Their values are ordinary step inputs — rendered in clear text in the +builder, persisted in the flow version, and stored in step inputs. Run-log input truncation +(`AP_FLOW_RUN_LOG_INPUT_TRUNCATE_THRESHOLD_KB`, 2 KB) does not help; a password is far under the +threshold. The prop descriptions say so, which is the only mitigation available today. + +There is no `Property.SecretText` to switch to. `SecretTextProperty` exists, but only as a +`PieceAuthProperty` reachable through `PieceAuth.SecretText`, and it is deliberately absent from the +`InputProperty` union that `createAction`'s `props` must satisfy — so it cannot be used as a step +input without a cast, and it carries auth-only concerns (`validate`, `getConnectionIdentifier`) that +make no sense on a step. + +Everything *downstream* of the authoring API already supports it: the builder renders +`PropertyType.SECRET_TEXT` with `type='password'` +(`packages/web/src/app/builder/piece-properties/properties-utils.tsx`), `piecePropertiesUtils.buildSchema` +validates it as a string, and the web form seeds it with `''`. Only the factory and the union entry +are missing. Adding them is a framework change worth proposing on its own merits for every piece — +not something to smuggle in here. + +Note that it would fix only the *display*. A step-level `SECRET_TEXT` value is still persisted +verbatim in the flow version, so removing passwords from flow storage altogether needs a +connection-based design, not a prop type. ## Internal-infrastructure escape hatch @@ -215,4 +320,15 @@ client secret always come from the connection. `fields[ustomers]` and a silently ignored fieldset. - `Serialize JSON:API Request` accepts a single-resource document and unwraps it, but **rejects a collection** (`data` is an array) with an explanatory error. Loop first. -- Piece i18n keys live in `src/i18n/translation.json` (identity-mapped English). +- Props created inside `Property.DynamicProperties` never reach piece metadata, so the line-item + field labels in `create-order.ts` and `create-invoice.ts` cannot be translated at all. Moving those + props out of `DynamicProperties` into a plain `Property.Array` is the only fix, and it is a + separate decision. +- `src/i18n/pl.json` and `src/i18n/uk.json` are never loaded. `pieceTranslation.initializeI18n` + iterates `LocalesEnum` (`packages/core/utils/src/lib/locale.ts`), which has no Polish or + Ukrainian. The gate keeps them in sync so they are ready if those locales are added, and + `i18n:check` prints a warning for each. +- Line-item input is validated through `lineItemUtils` (`src/lib/common/line-items.ts`), not bare + `Number()`. `Number(undefined)` is `NaN` and `JSON.stringify` serialises `NaN` as `null`, so an + unvalidated missing quantity used to reach Oro as `null` with no error. Route any new line-item + field through the helper. diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index 78d062a647e1..732992f976cf 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -1,6 +1,22 @@ { "name": "@activepieces/piece-orocommerce", "version": "0.2.0", + "description": "Activepieces piece for the OroCommerce back-office JSON:API: create and update customers, customer users, back-office users, orders and invoices, and trigger flows from OroCommerce webhooks.", + "keywords": [ + "activepieces", + "orocommerce", + "orocrm", + "b2b", + "ecommerce", + "json-api" + ], + "license": "MIT", + "homepage": "https://github.com/oroinc/activepieces/tree/main/packages/pieces/community/orocommerce#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/oroinc/activepieces.git", + "directory": "packages/pieces/community/orocommerce" + }, "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ b/packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ deleted file mode 100644 index 90f9a74a4e41..000000000000 --- a/packages/pieces/community/orocommerce/plan-oroCommerceActions.prompt.md_ +++ /dev/null @@ -1,784 +0,0 @@ -# OroCommerce Piece — Detailed Implementation Plan - -## Codebase Context - -### File Structure - -``` -packages/pieces/community/orocommerce/src/ -├── index.ts # Piece definition, registers all actions + triggers -├── lib/ -│ ├── actions/ -│ │ ├── index.ts # Re-exports all action modules -│ │ ├── api-call.ts # Custom API Call (utility) -│ │ ├── serialize-jsonapi.ts # Serialize JSON:API (utility) -│ │ ├── unserialize-jsonapi.ts # Unserialize JSON:API (utility) -│ │ ├── create-order.ts # ✅ Implemented -│ │ ├── create-invoice.ts # ✅ Implemented -│ │ ├── create-customer.ts # ✅ Implemented -│ │ ├── update-customer.ts # ✅ Implemented -│ │ ├── create-customer-user.ts # ✅ Implemented -│ │ └── update-customer-user.ts # ✅ Implemented -│ ├── common/ -│ │ ├── index.ts # Re-exports auth, client, props, address-props -│ │ ├── auth.ts # OAuth2 client_credentials auth -│ │ ├── client.ts # oroApiCall(), fetchCollection(), getOroBaseUrl(), getAccessToken() -│ │ ├── types.ts # OroAuth, OroApiCallParams, OroJsonApiItem, etc. -│ │ ├── props.ts # All dropdown props + additionalAttributes/Relations props -│ │ ├── address-props.ts # buildIncludedAddress(), baseAddressArrayItemProps, addressTypeProps -│ │ ├── jsonapi-body-utils.ts # pickDefined(), buildRels(), parseAdditionalAttributes/Relations() -│ │ └── jsonapi/ # Serialize/unserialize JSON:API helpers -│ ├── examples/ # Sample response data -│ └── triggers/ -│ └── webhook-topic-trigger.ts # Webhook trigger -``` - -### Key Architectural Patterns - -**Every action follows this exact pattern:** - -1. Import `createAction`, `Property` from `@activepieces/pieces-framework` -2. Import `HttpMethod` from `@activepieces/pieces-common` -3. Import `oroAuth`, `oroApiCall`, relevant dropdown props from `../common` -4. Import `OroAuth` from `../common/types` -5. Import `jsonApiBodyUtils` from `../common/jsonapi-body-utils` -6. Define `props` with required fields first, then optional attributes, then relationships, then `additionalAttributes` + `additionalRelations` last -7. In `run()`: extract `p = context.propsValue`, build `attributes` with `jsonApiBodyUtils.pickDefined()`, build `relationships` with `jsonApiBodyUtils.buildRels()`, merge extras, call `oroApiCall()`, return `response.body` - -**Create vs Update pattern:** - -- **Create**: `HttpMethod.POST`, resourceUri = `/entityname`, body includes `data.type` (no `id`) -- **Update**: `HttpMethod.PATCH`, resourceUri = `/entityname/${p.entityId}`, body includes `data.type` AND `data.id`, all fields optional - -**Dropdown prop types (in `props.ts`):** - -- `makeSearchableDropdown({...})` — for entities with server-side search (uses `filter[searchQuery]`) -- `makeEnumDropdown({...})` — for small enum/status lists (loads all, no search) -- Both use `oroAuth`, return `NOT_CONNECTED` / `FAILED` sentinels on error -- `attrLabel('attr1', 'attr2')` — creates label function that picks first non-null attribute - -**`jsonApiBodyUtils` helpers:** - -- `pickDefined({key: value})` — strips null/undefined entries -- `buildRels({relName: ['type', id]})` — builds JSON:API relationship linkage, skips null/empty ids -- `parseAdditionalAttributes(raw)` / `parseAdditionalRelations(raw)` — parse user JSON overrides -- `omitEmptyObjects(obj)` — strips keys with `{}` values - -**`buildIncludedAddress()` (from `address-props.ts`):** - -- Builds a JSON:API `included` resource for inline address creation -- Used for entities with address sub-resources (customers, customer users, orders) - -### Registration Checklist (for every new action) - -1. Create action file in `src/lib/actions/` -2. Add export to `src/lib/actions/index.ts` -3. Add import + registration in `src/index.ts` (both in the import block and the `actions: [...]` array) - ---- - -## Phase 1: Core Commerce (4 actions) - -### 1.1 Create Product — `create-product.ts` - -**API endpoint:** `POST /admin/api/products` -**JSON:API type:** `products` - -**Props:** - -| Prop Name | Type | Required | Description | -|------------------------|--------------------------------------------|----------|------------------------------------------------------------------------| -| `sku` | `ShortText` | ✅ | Product SKU code | -| `status` | `StaticDropdown` | ✅ | `enabled` / `disabled` (default: `disabled`) | -| `productType` | `StaticDropdown` | No | `simple` / `configurable` / `kit` (default: `simple`) | -| `featured` | `Checkbox` | No | Mark as featured | -| `newArrival` | `Checkbox` | No | Mark as new arrival | -| `availabilityDate` | `ShortText` | No | YYYY-MM-DDTHH:mm:ssZ | -| `categorySortOrder` | `Number` | No | Sort order in catalog category | -| `productName` | `ShortText` | ✅ | Default-locale product name (creates `productnames` included resource) | -| `productDescription` | `LongText` | No | Default-locale description (creates `productdescriptions` included) | -| `shortDescription` | `LongText` | No | Default-locale short description | -| `attributeFamily` | **New dropdown** `attributeFamilyDropdown` | ✅ | Required relationship | -| `productUnit` | Existing `productUnitDropdown` | ✅ | Primary unit (creates `productunitprecisions` included) | -| `unitPrecision` | `Number` | No | Decimal precision for the primary unit (default: 0) | -| `inventoryStatus` | **New dropdown** `inventoryStatusDropdown` | No | In Stock / Out of Stock | -| `category` | **New dropdown** `categoryDropdown` | No | Master catalog category | -| `brand` | **New dropdown** `brandDropdown` | No | Product brand | -| `taxCode` | **New dropdown** `productTaxCodeDropdown` | No | Product tax code | -| `organization` | Existing `organizationDropdown` | No | | -| `owner` | Existing `userDropdown` | No | | -| `additionalAttributes` | Existing | No | | -| `additionalRelations` | Existing | No | | - -**Implementation notes:** - -- Product creation requires `included` resources for localized fields (`productnames`, `productdescriptions`, `productshortdescriptions`). Each included resource must have: - ```json - { - "type": "productnames", - "id": "product_name_default", - "attributes": { "string": "Product Name", "fallback": null }, - "relationships": { "localization": { "data": null } } - } - ``` - Setting `localization.data` to `null` means "default locale." -- `primaryUnitPrecision` requires an included `productunitprecisions` resource: - ```json - { - "type": "productunitprecisions", - "id": "primary_unit", - "attributes": { "precision": 0, "conversionRate": 1, "sell": true }, - "relationships": { "unit": { "data": { "type": "productunits", "id": "each" } } } - } - ``` -- The relationship linkage for names/descriptions/shortDescriptions is an array of `{ type, id }` refs pointing to the included resources. - -**New dropdowns needed in `props.ts`:** - -``` -attributeFamilyDropdown → makeSearchableDropdown, /attributefamilies, fieldsParam: 'id,code', searchExpr: code ~ "q", labelFn: attrLabel('code'), required: true -productTaxCodeDropdown → makeSearchableDropdown, /producttaxcodes, fieldsParam: 'id,code', searchExpr: code ~ "q", labelFn: attrLabel('code') -inventoryStatusDropdown → makeEnumDropdown, /productinventorystatuses, labelFn: attrLabel('name') -categoryDropdown → makeSearchableDropdown, /mastercatalogcategories, fieldsParam: 'id,title', searchExpr: title ~ "q", labelFn: attrLabel('title') -brandDropdown → makeSearchableDropdown, /brands, fieldsParam: 'id', searchExpr: allText ~ "q", labelFn: item => item.id -``` - -### 1.2 Update Product — `update-product.ts` - -**API endpoint:** `PATCH /admin/api/products/{id}` -**JSON:API type:** `products` - -**Props:** Same as Create Product but: - -- Add `productId` (`ShortText`, required) — the ID to update -- All other fields become optional (including `sku`, `attributeFamily`) -- `productName`, `productDescription`, `shortDescription` — if provided, send as included resources same as create - -### 1.3 Update Order — `update-order.ts` - -**API endpoint:** `PATCH /admin/api/orders/{id}` -**JSON:API type:** `orders` - -**Props:** - -| Prop Name | Type | Required | -|--------------------------------|-------------------------------|----------| -| `orderId` | `ShortText` | ✅ | -| `currency` | `ShortText` | No | -| `poNumber` | `ShortText` | No | -| `customerNotes` | `LongText` | No | -| `shipUntil` | `ShortText` | No | -| `overriddenShippingCostAmount` | `Number` | No | -| `shippingMethod` | `ShortText` | No | -| `shippingMethodType` | `ShortText` | No | -| `disablePromotions` | `Checkbox` | No | -| `customer` | `customerDropdown` (optional) | No | -| `customerUser` | `customerUserDropdown(false)` | No | -| `organization` | `organizationDropdown` | No | -| `owner` | `userDropdown` | No | -| `website` | `websiteDropdown` | No | -| `internalStatus` | `orderInternalStatusDropdown` | No | -| `paymentTerm` | `paymentTermDropdown` | No | -| `warehouse` | `warehouseDropdown` | No | -| `status` | `orderStatusDropdown` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**Pattern:** Follow `update-customer.ts` exactly — `PATCH`, all fields optional, use `pickDefined` + `buildRels`. - -### 1.4 Update Invoice — `update-invoice.ts` - -**API endpoint:** `PATCH /admin/api/invoices/{id}` -**JSON:API type:** `invoices` - -**Props:** - -| Prop Name | Type | Required | -|------------------------|---------------------------------|----------| -| `invoiceId` | `ShortText` | ✅ | -| `invoiceDate` | `ShortText` | No | -| `currency` | `ShortText` | No | -| `customerName` | `ShortText` | No | -| `totalAmount` | `Number` | No | -| `invoiceNumber` | `ShortText` | No | -| `title` | `ShortText` | No | -| `description` | `LongText` | No | -| `memo` | `ShortText` | No | -| `billTo` | `LongText` | No | -| `shipTo` | `LongText` | No | -| `shippingMethod` | `ShortText` | No | -| `sellerInfo` | `LongText` | No | -| `externalPaymentUrl` | `ShortText` | No | -| `customer` | `customerDropdown` | No | -| `customerUser` | `customerUserDropdown(false)` | No | -| `organization` | `organizationDropdown` | No | -| `owner` | `userDropdown` | No | -| `website` | `websiteDropdown` | No | -| `internalStatus` | `invoiceInternalStatusDropdown` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - ---- - -## Phase 2: B2B Workflow (2 actions) - -### 2.1 Create Quote — `create-quote.ts` - -**API endpoint:** `POST /admin/api/quotes` -**JSON:API type:** `quotes` - -**Attributes from spec:** - -- `currency`, `poNumber`, `validUntil`, `shipUntil`, `projectName` - -**Relationships from spec:** - -- `customer` (customers), `customerUser` (customerusers), `organization` (organizations), `owner` (users), `website` (websites) -- `internalStatus` (quoteinternalstatuses), `customerStatus` (quotecustomerstatuses) -- `opportunity` (opportunities), `paymentTerm` (paymentterms) -- `quoteProducts` (quoteproducts) — line items via included resources -- `shippingAddress` (quoteshippingaddresses) — inline included address - -**Props:** - -| Prop Name | Type | Required | -|------------------------|----------------------------------------|--------------------| -| `currency` | `ShortText` | ✅ (default: 'USD') | -| `customer` | `customerRequiredDropdown` | ✅ | -| `poNumber` | `ShortText` | No | -| `validUntil` | `ShortText` | No | -| `shipUntil` | `ShortText` | No | -| `projectName` | `ShortText` | No | -| `customerUser` | `customerUserDropdown(false)` | No | -| `organization` | `organizationDropdown` | No | -| `owner` | `userDropdown` | No | -| `website` | `websiteDropdown` | No | -| `internalStatus` | **New** `quoteInternalStatusDropdown` | No | -| `customerStatus` | **New** `quoteCustomerStatusDropdown` | No | -| `opportunity` | **New** `opportunityDropdown` | No | -| `paymentTerm` | `paymentTermDropdown` | No | -| `shippingAddress*` | Address fields (same pattern as order) | No | -| `lineItems` | `DynamicProperties` → `Property.Array` | ✅ | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**Line item sub-props** (inside `Property.Array`): - -- `productSku` (ShortText, required), `productId` (ShortText), `freeFormProduct` (ShortText) -- `comment` (LongText) -- Quote products have `quoteProductOffers` which specify price tiers — each offer has: `quantity` (Number, required), `value` (Number, required), `currency` (ShortText), `productUnit` (StaticDropdown from fetched units), `allowIncrements` (Checkbox) - -**Included resources structure:** - -```json -{ - "type": "quoteproducts", - "id": "qp_1", - "attributes": { - "comment": "...", - "freeFormProduct": "..." - }, - "relationships": { - "product": { - "data": { - "type": "products", - "id": "42" - } - }, - "quoteProductOffers": { - "data": [ - { - "type": "quoteproductoffers", - "id": "qpo_1_1" - } - ] - } - } -} -``` - -And each offer: - -```json -{ - "type": "quoteproductoffers", - "id": "qpo_1_1", - "attributes": { - "quantity": 10, - "value": "99.99", - "currency": "USD", - "allowIncrements": false - }, - "relationships": { - "productUnit": { - "data": { - "type": "productunits", - "id": "each" - } - } - } -} -``` - -**New dropdowns needed:** - -``` -quoteInternalStatusDropdown → makeEnumDropdown, /quoteinternalstatuses, labelFn: attrLabel('name') -quoteCustomerStatusDropdown → makeEnumDropdown, /quotecustomerstatuses, labelFn: attrLabel('name') -opportunityDropdown → makeSearchableDropdown, /opportunities, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') -``` - -### 2.2 Create RFQ — `create-rfq.ts` - -**API endpoint:** `POST /admin/api/rfqs` -**JSON:API type:** `rfqs` - -**Attributes from spec:** - -- `firstName`, `lastName`, `email`, `company`, `phone`, `role` -- `poNumber`, `note`, `shipUntil`, `projectName` - -**Relationships:** - -- `customer` (customers), `customerUser` (customerusers), `organization` (organizations), `owner` (users), `website` (websites) -- `internal_status` (rfqinternalstatuses), `customer_status` (rfqcustomerstatuses) -- `requestProducts` (rfqproducts) — line items via included - -**Props:** - -| Prop Name | Type | Required | -|------------------------|-------------------------------|----------| -| `firstName` | `ShortText` | ✅ | -| `lastName` | `ShortText` | ✅ | -| `email` | `ShortText` | ✅ | -| `company` | `ShortText` | No | -| `phone` | `ShortText` | No | -| `role` | `ShortText` | No | -| `poNumber` | `ShortText` | No | -| `note` | `LongText` | No | -| `shipUntil` | `ShortText` | No | -| `projectName` | `ShortText` | No | -| `customer` | `customerDropdown` | No | -| `customerUser` | `customerUserDropdown(false)` | No | -| `organization` | `organizationDropdown` | No | -| `owner` | `userDropdown` | No | -| `website` | `websiteDropdown` | No | -| `lineItems` | `Property.Array` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**Line item sub-props:** - -- `productSku` (ShortText, required), `productId` (ShortText), `comment` (LongText) -- Each RFQ product has `rfqProductItems` — each item has: `quantity` (Number, required), `value` (Number), `currency` (ShortText), `productUnit` (ShortText — use code like "each") - -**Included resources:** - -```json -{ - "type": "rfqproducts", - "id": "rp_1", - "relationships": { - "product": { - "data": { - "type": "products", - "id": "42" - } - }, - "requestProductItems": { - "data": [ - { - "type": "rfqproductitems", - "id": "rpi_1_1" - } - ] - } - } -} -``` - ---- - -## Phase 3: Pricing & Fulfillment (5 actions) - -### 3.1 Create Price List — `create-price-list.ts` - -**API endpoint:** `POST /admin/api/pricelists` -**JSON:API type:** `pricelists` - -**Props:** - -| Prop Name | Type | Required | -|-------------------------|------------------------|--------------------| -| `name` | `ShortText` | ✅ | -| `active` | `Checkbox` | No (default: true) | -| `productAssignmentRule` | `LongText` | No | -| `organization` | `organizationDropdown` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -Simple action — no included resources needed. - -### 3.2 Update Price List — `update-price-list.ts` - -**API endpoint:** `PATCH /admin/api/pricelists/{id}` -Same props as create but all optional + `priceListId` required. - -### 3.3 Create Product Price — `create-product-price.ts` - -**API endpoint:** `POST /admin/api/productprices` -**JSON:API type:** `productprices` - -**Props:** - -| Prop Name | Type | Required | -|------------------------|-----------------------------|--------------------| -| `value` | `Number` | ✅ | -| `currency` | `ShortText` | ✅ (default: 'USD') | -| `quantity` | `Number` | ✅ | -| `priceList` | **New** `priceListDropdown` | ✅ | -| `product` | `productDropdown` | ✅ | -| `unit` | `productUnitDropdown` | ✅ | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -Simple action — three required relationships, three required attributes. - -**New dropdown:** - -``` -priceListDropdown → makeSearchableDropdown, /pricelists, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') -``` - -### 3.4 Update Product Price — `update-product-price.ts` - -**API endpoint:** `PATCH /admin/api/productprices/{id}` -Same props but all optional + `productPriceId` required. - -### 3.5 Create Shipping Tracking — `create-shipping-tracking.ts` - -**API endpoint:** `POST /admin/api/ordershippingtrackings` -**JSON:API type:** `ordershippingtrackings` - -**Props:** - -| Prop Name | Type | Required | -|------------------------|--------------------------------------------------|----------| -| `method` | `ShortText` | ✅ | -| `number` | `ShortText` | ✅ | -| `order` | `orderDropdown` (modify to use required variant) | ✅ | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -Very simple action — 2 attributes, 1 relationship (note: the relationship key is `orders` plural per the spec, and the linkage uses `{ type: 'orders', id }` with a many-to-one wrapper `{ data: [{ type: 'orders', id }] }`). - ---- - -## Phase 4: CRM Entities (6 actions) - -### 4.1 Create Contact — `create-contact.ts` - -**API endpoint:** `POST /admin/api/contacts` -**JSON:API type:** `contacts` - -**Props:** - -| Prop Name | Type | Required | -|------------------------|---------------------------------------------------|----------------------------------------------------------------------------| -| `firstName` | `ShortText` | No (conditionally required: either firstName, lastName, emails, or phones) | -| `lastName` | `ShortText` | No | -| `primaryEmail` | `ShortText` | No | -| `primaryPhone` | `ShortText` | No | -| `emails` | `Property.Json` | No — array of `[{"email": "..."}]` | -| `phones` | `Property.Json` | No — array of `[{"phone": "..."}]` | -| `jobTitle` | `ShortText` | No | -| `description` | `LongText` | No | -| `namePrefix` | `ShortText` | No | -| `middleName` | `ShortText` | No | -| `nameSuffix` | `ShortText` | No | -| `gender` | `StaticDropdown` | No — `male` / `female` | -| `birthday` | `ShortText` | No — YYYY-MM-DD | -| `fax` | `ShortText` | No | -| `skype` | `ShortText` | No | -| `twitter` | `ShortText` | No | -| `facebook` | `ShortText` | No | -| `linkedIn` | `ShortText` | No | -| `externalId` | `ShortText` | No | -| `source` | **New** `contactSourceDropdown` | No | -| `method` | **New** `contactMethodDropdown` | No | -| `owner` | `userDropdown` | No | -| `organization` | `organizationDropdown` | No | -| `addresses` | `Property.Array` with `baseAddressArrayItemProps` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**Contact-specific notes:** - -- `emails` and `phones` are structured arrays in the JSON:API attributes (not relationships). Format: `[{"email": "addr@example.com"}]` and `[{"phone": "555-1234"}]` -- `source` is a relationship to `contactsources` (enumerable) -- `method` is a relationship to `contactmethods` (enumerable) -- Addresses use `contactaddresses` type in included resources - -**New dropdowns:** - -``` -contactSourceDropdown → makeEnumDropdown, /contactsources, labelFn: attrLabel('label') -contactMethodDropdown → makeEnumDropdown, /contactmethods, labelFn: attrLabel('label') -contactDropdown → makeSearchableDropdown, /contacts, fieldsParam: 'id,firstName,lastName,primaryEmail', searchExpr: allText ~ "q", labelFn: composite name+email -``` - -### 4.2 Update Contact — `update-contact.ts` - -Same props as Create Contact but all optional + `contactId` (ShortText, required). No address creation on update (addresses are managed separately). Follow Update Customer pattern. - -### 4.3 Create Lead — `create-lead.ts` - -**API endpoint:** `POST /admin/api/leads` -**JSON:API type:** `leads` - -**Props:** - -| Prop Name | Type | Required | -|------------------------|---------------------------------------------------|---------------------------------------------------------------| -| `name` | `ShortText` | ✅ | -| `firstName` | `ShortText` | No | -| `lastName` | `ShortText` | No | -| `companyName` | `ShortText` | No | -| `primaryEmail` | `ShortText` | No | -| `primaryPhone` | `ShortText` | No | -| `emails` | `Property.Json` | No | -| `phones` | `Property.Json` | No | -| `jobTitle` | `ShortText` | No | -| `website` | `ShortText` | No (note: this is a text attribute, not the website dropdown) | -| `namePrefix` | `ShortText` | No | -| `middleName` | `ShortText` | No | -| `nameSuffix` | `ShortText` | No | -| `numberOfEmployees` | `Number` | No | -| `industry` | `ShortText` | No | -| `twitter` | `ShortText` | No | -| `linkedIn` | `ShortText` | No | -| `notes` | `LongText` | No | -| `externalId` | `ShortText` | No | -| `status` | **New** `leadStatusDropdown` | No | -| `source` | **New** `leadSourceDropdown` | No | -| `contact` | **New** `contactDropdown` | No | -| `customer` | `customerDropdown` | No | -| `account` | **New** `accountDropdown` | No | -| `owner` | `userDropdown` | No | -| `organization` | `organizationDropdown` | No | -| `addresses` | `Property.Array` with `baseAddressArrayItemProps` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**Lead-specific notes:** - -- Lead addresses use type `leadaddresses` in included resources -- `emails`/`phones` same structure as contacts - -**New dropdowns:** - -``` -leadStatusDropdown → makeEnumDropdown, /leadstatuses, labelFn: attrLabel('name') -leadSourceDropdown → makeEnumDropdown, /leadsources, labelFn: attrLabel('name') -accountDropdown → makeSearchableDropdown, /accounts, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') -leadDropdown → makeSearchableDropdown, /leads, fieldsParam: 'id,name', searchExpr: name ~ "q", labelFn: attrLabel('name') -``` - -### 4.4 Update Lead — `update-lead.ts` - -Same as Create Lead but all optional + `leadId` required. No address creation. Follow Update Customer pattern. - -### 4.5 Create Opportunity — `create-opportunity.ts` - -**API endpoint:** `POST /admin/api/opportunities` -**JSON:API type:** `opportunities` - -**Props:** - -| Prop Name | Type | Required | -|------------------------|------------------------------------------|-----------------| -| `name` | `ShortText` | ✅ | -| `budgetAmountValue` | `Number` | No | -| `budgetAmountCurrency` | `ShortText` | No | -| `closeDate` | `ShortText` | No — YYYY-MM-DD | -| `closeRevenueValue` | `Number` | No | -| `closeRevenueCurrency` | `ShortText` | No | -| `probability` | `Number` | No — 0.0 to 1.0 | -| `customerNeed` | `LongText` | No | -| `proposedSolution` | `LongText` | No | -| `notes` | `LongText` | No | -| `externalId` | `ShortText` | No | -| `status` | **New** `opportunityStatusDropdown` | No | -| `closeReason` | **New** `opportunityCloseReasonDropdown` | No | -| `contact` | `contactDropdown` | No | -| `customer` | `customerDropdown` | No | -| `account` | `accountDropdown` | No | -| `lead` | `leadDropdown` | No | -| `owner` | `userDropdown` | No | -| `organization` | `organizationDropdown` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**New dropdowns:** - -``` -opportunityStatusDropdown → makeEnumDropdown, /opportunitystatuses, labelFn: attrLabel('name') -opportunityCloseReasonDropdown → makeEnumDropdown, /opportunityclosereasons, labelFn: attrLabel('label') -``` - -### 4.6 Update Opportunity — `update-opportunity.ts` - -Same as Create but all optional + `opportunityId` required. Follow Update Customer pattern. - ---- - -## Phase 5: Activities & Accounts (5 actions) - -### 5.1 Create Account — `create-account.ts` - -**API endpoint:** `POST /admin/api/accounts` -**JSON:API type:** `accounts` - -**Props:** - -| Prop Name | Type | Required | -|------------------------|------------------------|----------| -| `name` | `ShortText` | ✅ | -| `externalId` | `ShortText` | No | -| `defaultContact` | `contactDropdown` | No | -| `organization` | `organizationDropdown` | No | -| `owner` | `userDropdown` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -### 5.2 Update Account — `update-account.ts` - -Same as Create but all optional + `accountId` required. - -### 5.3 Create Task — `create-task.ts` - -**API endpoint:** `POST /admin/api/tasks` -**JSON:API type:** `tasks` - -**Props (verify against spec):** - -| Prop Name | Type | Required | -|------------------------|-----------------------------------|---------------| -| `subject` | `ShortText` | ✅ | -| `description` | `LongText` | No | -| `dueDate` | `ShortText` | No — datetime | -| `priority` | `StaticDropdown` or enum dropdown | No | -| `status` | Enum dropdown (`/taskstatuses`) | No | -| `owner` | `userDropdown` | No | -| `organization` | `organizationDropdown` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**⚠️ Verify against spec:** Check `/admin/api/tasks` POST schema and `/admin/api/taskstatuses`, `/admin/api/taskpriorities` endpoints before implementing. The endpoint names need validation. - -### 5.4 Create Call — `create-call.ts` - -**API endpoint:** `POST /admin/api/calls` -**JSON:API type:** `calls` - -**Props (verify against spec):** - -| Prop Name | Type | Required | -|------------------------|------------------------|------------------------------| -| `subject` | `ShortText` | ✅ | -| `phoneNumber` | `ShortText` | No | -| `notes` | `LongText` | No | -| `callDateTime` | `ShortText` | No — datetime | -| `duration` | `Number` | No — seconds or formatted | -| `direction` | `StaticDropdown` | No — `incoming` / `outgoing` | -| `owner` | `userDropdown` | No | -| `organization` | `organizationDropdown` | No | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - -**⚠️ Verify against spec:** Check `/admin/api/calls` POST schema. The spec shows `calls` has POST support. Verify exact attribute names and required fields. - -### 5.5 Create Calendar Event — `create-calendar-event.ts` - -**⚠️ Verify against spec:** Search for `calendarevents` or `calendar` paths in the spec. Calendar events may use a different path pattern. Verify availability before implementing. - -**Likely props:** - -| Prop Name | Type | Required | -|------------------------|-------------|--------------| -| `title` | `ShortText` | ✅ | -| `description` | `LongText` | No | -| `start` | `ShortText` | ✅ — datetime | -| `end` | `ShortText` | ✅ — datetime | -| `allDay` | `Checkbox` | No | -| `calendar` | Dropdown | ✅ | -| `additionalAttributes` | Existing | No | -| `additionalRelations` | Existing | No | - ---- - -## Summary of All New Dropdown Props - -Add these to `props.ts` between the `customerUserRoleDropdown` section and the `additionalAttributesProp` section, following the existing code style: - -| Export Name | Builder | URI | Fields | Search | Label | -|----------------------------------|--------------------------|-----------------------------|--------------------------------------|-----------------|----------------------| -| `attributeFamilyDropdown` | `makeSearchableDropdown` | `/attributefamilies` | `id,code` | `code ~ "q"` | `attrLabel('code')` | -| `productTaxCodeDropdown` | `makeSearchableDropdown` | `/producttaxcodes` | `id,code` | `code ~ "q"` | `attrLabel('code')` | -| `inventoryStatusDropdown` | `makeEnumDropdown` | `/productinventorystatuses` | — | — | `attrLabel('name')` | -| `categoryDropdown` | `makeSearchableDropdown` | `/mastercatalogcategories` | `id,title` | `title ~ "q"` | `attrLabel('title')` | -| `brandDropdown` | `makeSearchableDropdown` | `/brands` | `id` | `allText ~ "q"` | `(item) => item.id` | -| `contactGroupDropdown` | `makeSearchableDropdown` | `/contactgroups` | `id,label` | `label ~ "q"` | `attrLabel('label')` | -| `contactSourceDropdown` | `makeEnumDropdown` | `/contactsources` | — | — | `attrLabel('label')` | -| `contactMethodDropdown` | `makeEnumDropdown` | `/contactmethods` | — | — | `attrLabel('label')` | -| `contactDropdown` | `makeSearchableDropdown` | `/contacts` | `id,firstName,lastName,primaryEmail` | `allText ~ "q"` | composite fn | -| `leadStatusDropdown` | `makeEnumDropdown` | `/leadstatuses` | — | — | `attrLabel('name')` | -| `leadSourceDropdown` | `makeEnumDropdown` | `/leadsources` | — | — | `attrLabel('name')` | -| `accountDropdown` | `makeSearchableDropdown` | `/accounts` | `id,name` | `name ~ "q"` | `attrLabel('name')` | -| `leadDropdown` | `makeSearchableDropdown` | `/leads` | `id,name` | `name ~ "q"` | `attrLabel('name')` | -| `opportunityStatusDropdown` | `makeEnumDropdown` | `/opportunitystatuses` | — | — | `attrLabel('name')` | -| `opportunityCloseReasonDropdown` | `makeEnumDropdown` | `/opportunityclosereasons` | — | — | `attrLabel('label')` | -| `quoteInternalStatusDropdown` | `makeEnumDropdown` | `/quoteinternalstatuses` | — | — | `attrLabel('name')` | -| `quoteCustomerStatusDropdown` | `makeEnumDropdown` | `/quotecustomerstatuses` | — | — | `attrLabel('name')` | -| `opportunityDropdown` | `makeSearchableDropdown` | `/opportunities` | `id,name` | `name ~ "q"` | `attrLabel('name')` | -| `priceListDropdown` | `makeSearchableDropdown` | `/pricelists` | `id,name` | `name ~ "q"` | `attrLabel('name')` | - ---- - -## Final Checklist Per Action File - -For **every** new action file: - -1. [ ] Create `src/lib/actions/.ts` -2. [ ] Add export in `src/lib/actions/index.ts` -3. [ ] Add import + array entry in `src/index.ts` -4. [ ] Follow exact pattern: imports → createAction → helper functions → types (at end of file) -5. [ ] Use `jsonApiBodyUtils.pickDefined()` for optional attributes -6. [ ] Use `jsonApiBodyUtils.buildRels()` for optional relationships -7. [ ] Always include `additionalAttributes` + `additionalRelations` as last two props -8. [ ] Parse extras with `jsonApiBodyUtils.parseAdditionalAttributes()` / `parseAdditionalRelations()` -9. [ ] Spread extras into attributes/relationships with `...extraAttrs` / `...extraRels` -10. [ ] Return `response.body` from `run()` -11. [ ] Run `npx turbo run lint --filter=orocommerce` after each phase - ---- - -## Spec Verification Commands - -Before implementing Phase 5, verify these endpoints exist in the spec: - -```bash -# Tasks -jq '.paths["/admin/api/tasks"].post.summary' /home/dkhrysev/Downloads/specification.json -jq -r '.paths | keys[] | select(test("taskstatus|taskpriori"; "i"))' /home/dkhrysev/Downloads/specification.json - -# Calls -jq '.paths["/admin/api/calls"].post.summary' /home/dkhrysev/Downloads/specification.json - -# Calendar events -jq -r '.paths | keys[] | select(test("calendar"; "i"))' /home/dkhrysev/Downloads/specification.json - -# For each entity, get complete schema: -# jq '.components.schemas..properties.attributes.properties | keys' /home/dkhrysev/Downloads/specification.json -``` - diff --git a/packages/pieces/community/orocommerce/src/index.ts b/packages/pieces/community/orocommerce/src/index.ts index bfd1ffcd685e..0ba3f610a2bb 100644 --- a/packages/pieces/community/orocommerce/src/index.ts +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -19,8 +19,8 @@ import { export const orocommerce = createPiece({ displayName: 'OroCommerce', auth: oroAuth, - minimumSupportedRelease: '0.36.1', - logoUrl: 'https://static.oroinc.com/logo/O(logo).svg', + minimumSupportedRelease: '0.86.0', + logoUrl: 'https://static.oroinc.com/logo/O%28logo%29.svg', categories: [PieceCategory.COMMERCE], description: 'B2B digital commerce solution', authors: ['Oro Inc.'], From 36372f456b94892e07dbb626fd9d63b5733a353f Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Tue, 18 Aug 2026 12:16:05 +0200 Subject: [PATCH 36/50] fix(orocommerce): tighten line-item validation and drop the unsafe casts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A review of the five preceding commits turned up a handful of things that pass the suite but would misbehave in front of a user. This fixes the ones that do not need a product decision. Line items: - parseNumber still handed the string to Number(), so '0x10' became a quantity of 16 and '0b101' became 5. It now requires a decimal shape first. '+5', '-5', '.5', '5.' and '1e3' still parse. - readRows reported "add at least one row" for anything that was not an array, so a lineItems field mapped to a string told the user to add a row they had already added. Not-a-list and empty-list are now separate messages; the empty cases keep the old wording. - toMinorUnits used Math.round(value * 100), which is not decimal-exact: Math.round(1.005 * 100) is 100, not 101, so a total that was correct to the cent was rejected whenever toleranceMinorUnits was 0. create-invoice passes 1 and absorbed it, but 0 is the default and the next caller would not have. Checked across a matrix of amounts: the only behaviour that changes is at tolerance 0, and only half-cent pairings that were always correct now pass. Casts: - `p.additionalHeaders as Record` claimed every value in an Object property is a string. It is Record and the engine does not narrow it, so {"X-Foo": 123} reached the HTTP client as a number. All eight call sites now use toHeaderRecord, which the piece already applies to the connection's own headers. - `context.auth as OroAuth` was doing nothing in six actions — create-invoice, create-order and the trigger pass context.auth unchanged and compile fine. Removed, along with the imports that only existed to serve them. Also: - create-order read a `warehouseId` line-item field that no prop defines, so the first half of the `??` could never be satisfied. - create-invoice carried a comment copied from create-order describing a StaticDropdown of product units loaded once. This file loads nothing and its Product Unit is a ShortText. Removed; the DynamicProperties wrapper is left alone because unwrapping it would move the line-item labels into piece metadata and change the i18n key set. - The webhook trigger's onDisable deleted the webhook in Oro but left webhookInfo in the store. Harmless while it only holds an id; not harmless once it holds a secret. - check-i18n reported a malformed locale file as a bare "Unexpected end of JSON input" with six candidate files and no hint which. It now names the path. --write reported dropped keys as a count, which is no help when the count is a dozen human translations; it now lists them. - The i18n gate's own tool shipped unlinted, because eslint could not parse .mjs here at all. Added the override and widened the lint glob. - Dropped the @activepieces/shared alias from vitest.config.ts. Nothing imports it, the framework does not need it transitively, and the piece's own eslintrc bans that import. - New test/body-utils.test.ts: the helpers that decide what actually goes on the wire had no tests, including every throw branch of the two Additional* parsers. The README's claim that this piece keeps explanatory prose out of the source was not true (60 section markers and 15 prose comments), and its claim that i18n:write is "the same thing" as the CLI generator was not either — the CLI writes one file, i18n:write writes six, and they disagree on the trailing newline. Both corrected, and the checkbox gotcha below is recorded there. Left alone deliberately: an untouched Property.Checkbox is persisted as false, not undefined, so update-user and update-customer-user send enabled: false on every call and assertUpdateNotEmpty can never fire for them. The fix is a three-state prop, which changes the props and forces an i18n regeneration, so it wants its own change. Written up under Gotchas in the README with the mechanism and why a defaultValue cannot fix it. Co-Authored-By: Claude Opus 5 (1M context) --- .../community/orocommerce/.eslintrc.json | 14 ++ .../pieces/community/orocommerce/README.md | 42 +++-- .../pieces/community/orocommerce/package.json | 2 +- .../src/lib/actions/create-customer-user.ts | 6 +- .../src/lib/actions/create-customer.ts | 6 +- .../src/lib/actions/create-invoice.ts | 5 +- .../src/lib/actions/create-order.ts | 6 +- .../src/lib/actions/create-user.ts | 6 +- .../src/lib/actions/update-customer-user.ts | 6 +- .../src/lib/actions/update-customer.ts | 6 +- .../src/lib/actions/update-user.ts | 6 +- .../orocommerce/src/lib/common/line-items.ts | 11 +- .../src/lib/triggers/webhook-topic-trigger.ts | 15 +- .../orocommerce/test/body-utils.test.ts | 149 ++++++++++++++++++ .../orocommerce/test/line-items.test.ts | 50 ++++++ .../orocommerce/tools/check-i18n.mjs | 16 +- .../community/orocommerce/vitest.config.ts | 1 - 17 files changed, 297 insertions(+), 50 deletions(-) create mode 100644 packages/pieces/community/orocommerce/test/body-utils.test.ts diff --git a/packages/pieces/community/orocommerce/.eslintrc.json b/packages/pieces/community/orocommerce/.eslintrc.json index 01f1c80f85a4..89ae4dffbfff 100644 --- a/packages/pieces/community/orocommerce/.eslintrc.json +++ b/packages/pieces/community/orocommerce/.eslintrc.json @@ -42,6 +42,20 @@ "*.jsx" ], "rules": {} + }, + { + "files": [ + "*.mjs" + ], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "env": { + "node": true, + "es2022": true + }, + "rules": {} } ] } diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 06c98859c7b2..92ce06f6f9a3 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -60,10 +60,10 @@ tokens or customer data. --- -The rest of this file is for contributors. The piece follows the repository convention of keeping -explanatory prose out of the source, so the non-obvious rules live here — read the relevant section -before changing anything under `src/`. Most of them exist because of a bug that is easy to -reintroduce. +The rest of this file is for contributors. The repository bans code comments; this piece keeps +section markers and a handful of short why-comments anyway, and everything longer lives here — read +the relevant section before changing anything under `src/`. Most of them exist because of a bug that +is easy to reintroduce. ## How it talks to Oro @@ -250,7 +250,9 @@ build is the type-check. `.github/workflows/ci.yml` runs `test` and `i18n:check` for this piece by name, alongside the other packages in its hardcoded filter list. `test/jsonapi-roundtrip.test.ts` guards the -serialize/deserialize contract above and `test/line-items.test.ts` guards line-item validation. +serialize/deserialize contract above, `test/line-items.test.ts` guards line-item validation, +`test/body-utils.test.ts` guards the request-body helpers, and `test/action-guards.test.ts` guards +the checks that stop an action calling Oro with unusable input. ## The i18n gate @@ -258,21 +260,27 @@ serialize/deserialize contract above and `test/line-items.test.ts` guards line-i translations. Both are generated, not hand-maintained: ```bash -npm run cli pieces generate-translation-file orocommerce # canonical, inside the monorepo -npm run i18n:write # same thing without the monorepo CLI +npm run cli pieces generate-translation-file orocommerce # canonical; writes translation.json only +npx turbo run i18n:write --filter=@activepieces/piece-orocommerce # also reconciles the locale files (builds first) ``` +The two are not interchangeable. The CLI rewrites `translation.json` and nothing else, and it writes +no trailing newline; `i18n:write` rewrites all six files and does. Prefer `i18n:write` — it is the +one that keeps the locale files in step with the source. + `npm run i18n:check` (`tools/check-i18n.mjs`) fails the build when they drift. It imports the -**built** piece from `dist/`, walks the same 19 metadata paths as +**built** piece from `dist/` and only checks that the file exists, never that it is current — run it +through turbo (`npx turbo run i18n:check`), which builds first. It walks the same 19 metadata paths as `pieceTranslation.pathsToValuesToTranslate` in `packages/pieces/framework/src/lib/i18n.ts`, and truncates keys at 512 characters exactly as the official generator does. It fails on keys missing from or stale in `translation.json`, on any locale file whose key set differs from it, and on empty values. Values identical to the English source are a warning; `--strict-untranslated` promotes them to errors. -`npm run i18n:write` regenerates `translation.json` and reconciles every locale file against it — -stale keys are dropped, missing keys are seeded with the English text, and existing translations are -left untouched. Seeded keys still need translating. +`i18n:write` regenerates `translation.json` and reconciles every locale file against it — stale keys +are dropped, missing keys are seeded with the English text, and existing translations are left +untouched. Dropped keys are listed, because a key disappears whenever its English source text +changes and the translation attached to it goes with it. Seeded keys still need translating. ## Passwords are step inputs, and step inputs are not secrets @@ -328,6 +336,18 @@ client secret always come from the connection. iterates `LocalesEnum` (`packages/core/utils/src/lib/locale.ts`), which has no Polish or Ukrainian. The gate keeps them in sync so they are ready if those locales are added, and `i18n:check` prints a warning for each. +- **An untouched `Property.Checkbox` arrives as `false`, not `undefined`.** The builder seeds an + unset checkbox with `property.defaultValue ?? false` + (`packages/web/src/features/pieces/utils/form-utils.tsx`) and persists it into the step input, and + `checkboxProcessor` passes `false` through — it is the one property type whose "empty" form value is + not normalised to `undefined` the way `textProcessor` and `numberProcessor` normalise theirs. So + `p.enabled ?? undefined` is `false`, `pickDefined` keeps it, and `update-user` / `update-customer-user` + send `enabled: false` on every call while `assertUpdateNotEmpty` can never fire for them. Fixing it + means a three-state prop (a `Property.StaticDropdown` with a "leave unchanged" option, as in + `campaign-monitor/src/lib/actions/update-subscriber-details.ts`), which changes the props and so + needs an i18n regeneration — do not paper over it with a `defaultValue`, since `true` would + unconditionally *enable* instead. Hand-written `propsValue` in `test/action-guards.test.ts` does not + reproduce this; a builder-shaped case must include `enabled: false`. - Line-item input is validated through `lineItemUtils` (`src/lib/common/line-items.ts`), not bare `Number()`. `Number(undefined)` is `NaN` and `JSON.stringify` serialises `NaN` as `null`, so an unvalidated missing quantity used to reach Oro as `null` with no error. Route any new line-item diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index 732992f976cf..e4090d34078b 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -27,7 +27,7 @@ }, "scripts": { "build": "tsc -p tsconfig.lib.json && cp package.json dist/", - "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'", + "lint": "eslint 'src/**/*.ts' 'test/**/*.ts' 'tools/**/*.mjs'", "bundle": "node ../../../../dist/packages/cli/src/index.js pieces bundle", "test": "vitest run", "i18n:check": "node tools/check-i18n.mjs", diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts index 4ba38b7ffcf8..b8a745146385 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -15,8 +15,8 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, } from '../common'; -import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi'; export const createCustomerUserAction = createAction({ @@ -181,9 +181,9 @@ export const createCustomerUserAction = createAction({ const response = await oroApiCall({ method: HttpMethod.POST, resourceUri: '/customerusers', - auth: context.auth as OroAuth, + auth: context.auth, body, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts index 0e8bc5fd83ac..14bc4736230f 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts @@ -17,8 +17,8 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, } from '../common'; -import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi'; export const createCustomerAction = createAction({ @@ -139,9 +139,9 @@ export const createCustomerAction = createAction({ const response = await oroApiCall({ method: HttpMethod.POST, resourceUri: '/customers', - auth: context.auth as OroAuth, + auth: context.auth, body, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index 1efc014cd853..f1b52e63d9b2 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -12,6 +12,7 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, lineItemUtils, } from '../common'; import { jsonApiBodyUtils } from '../common/jsonapi'; @@ -126,8 +127,6 @@ export const createInvoiceAction = createAction({ internalStatus: invoiceInternalStatusDropdown, // -- Line Items ------------------------------------------------------------ - // DynamicProperties lets us load product units once as StaticDropdown. - // Products are entered as SKU/ID text (framework does not support Dropdown inside Array). lineItems: Property.DynamicProperties({ auth: oroAuth, displayName: 'Line Items', @@ -341,7 +340,7 @@ export const createInvoiceAction = createAction({ }, included, }, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts index 7a88659acbc6..83d9ec64f7f7 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -23,6 +23,7 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, lineItemUtils, } from '../common'; import { jsonApiBodyUtils } from '../common/jsonapi'; @@ -464,7 +465,7 @@ export const createOrderAction = createAction({ }, included, }, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; @@ -632,8 +633,7 @@ function buildOrderLineItem({ ], warehouse: [ 'warehouses', - orderText({ row, index, field: 'warehouseId', label: 'Warehouse' }) ?? - orderText({ row, index, field: 'warehouse', label: 'Warehouse' }), + orderText({ row, index, field: 'warehouse', label: 'Warehouse' }), ], }); diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts index 2604341dfd3a..292b245c0651 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts @@ -9,12 +9,12 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, userRolesMultiDropdown, userGroupsMultiDropdown, organizationsMultiDropdown, businessUnitsMultiDropdown, } from '../common'; -import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi'; export const createUserAction = createAction({ @@ -145,7 +145,7 @@ export const createUserAction = createAction({ const response = await oroApiCall({ method: HttpMethod.POST, resourceUri: '/users', - auth: context.auth as OroAuth, + auth: context.auth, body: { data: { type: 'users', @@ -153,7 +153,7 @@ export const createUserAction = createAction({ relationships, }, }, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index bd32cb926083..aa6b7ba1d169 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -11,8 +11,8 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, } from '../common'; -import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi'; export const updateCustomerUserAction = createAction({ @@ -140,7 +140,7 @@ export const updateCustomerUserAction = createAction({ const response = await oroApiCall({ method: HttpMethod.PATCH, resourceUri: `/customerusers/${p.customerUserId}`, - auth: context.auth as OroAuth, + auth: context.auth, body: { data: { type: 'customerusers', @@ -149,7 +149,7 @@ export const updateCustomerUserAction = createAction({ relationships, }, }, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts index 4f4115f7f582..e647e34adabe 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts @@ -13,8 +13,8 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, } from '../common'; -import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi'; export const updateCustomerAction = createAction({ @@ -103,7 +103,7 @@ export const updateCustomerAction = createAction({ const response = await oroApiCall({ method: HttpMethod.PATCH, resourceUri: `/customers/${p.customerId}`, - auth: context.auth as OroAuth, + auth: context.auth, body: { data: { type: 'customers', @@ -112,7 +112,7 @@ export const updateCustomerAction = createAction({ relationships, }, }, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts index 86cc3c112fd3..0a626de588d3 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -13,8 +13,8 @@ import { additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, + toHeaderRecord, } from '../common'; -import { OroAuth } from '../common/types'; import { jsonApiBodyUtils } from '../common/jsonapi'; export const updateUserAction = createAction({ @@ -150,7 +150,7 @@ export const updateUserAction = createAction({ const response = await oroApiCall({ method: HttpMethod.PATCH, resourceUri: `/users/${p.userId}`, - auth: context.auth as OroAuth, + auth: context.auth, body: { data: { type: 'users', @@ -159,7 +159,7 @@ export const updateUserAction = createAction({ relationships, }, }, - headers: p.additionalHeaders as Record, + headers: toHeaderRecord({ value: p.additionalHeaders }), }); return response.body; diff --git a/packages/pieces/community/orocommerce/src/lib/common/line-items.ts b/packages/pieces/community/orocommerce/src/lib/common/line-items.ts index ad2205ef5e1f..f69083850b1e 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/line-items.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/line-items.ts @@ -33,7 +33,7 @@ function describeValue(value: unknown): string { } function toMinorUnits(value: number): number { - return Math.round(value * 100); + return Math.round(Number((value * 100).toFixed(4))); } function parseNumber(value: unknown): number | undefined { @@ -42,7 +42,7 @@ function parseNumber(value: unknown): number | undefined { } if (typeof value === 'string') { const trimmed = value.trim(); - if (trimmed === '') return undefined; + if (!DECIMAL_NUMBER.test(trimmed)) return undefined; const parsed = Number(trimmed); return Number.isFinite(parsed) ? parsed : undefined; } @@ -58,6 +58,11 @@ function readRows({ const raw = container?.[arrayKey] ?? (Array.isArray(value) ? value : undefined); + if (raw !== undefined && raw !== null && !Array.isArray(raw)) { + throw new Error( + `${displayName}: expected a list of rows, got ${describeValue(raw)}.` + ); + } if (!Array.isArray(raw) || raw.length === 0) { throw new Error( `${displayName}: add at least one row before running this step.` @@ -202,6 +207,8 @@ export const lineItemUtils = { assertSumMatches, }; +const DECIMAL_NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/; + export type LineItemRow = Record; type ReadRowsParams = { diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index 557931c2764e..c1bddca1f9a6 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -100,15 +100,16 @@ export const oroWebhookTopicTrigger = createTrigger({ throwOriginalError: true, }); } catch (error: unknown) { - if (error instanceof HttpError) { - // Skip already removed webhooks and access denied errors - if ([401, 403, 404].includes(error.response.status)) { - return; - } - } + const alreadyGone = + error instanceof HttpError && + [401, 403, 404].includes(error.response.status); - throw new Error(formatError({ error })); + if (!alreadyGone) { + throw new Error(formatError({ error })); + } } + + await context.store.delete('webhookInfo'); } }, diff --git a/packages/pieces/community/orocommerce/test/body-utils.test.ts b/packages/pieces/community/orocommerce/test/body-utils.test.ts new file mode 100644 index 000000000000..fdd2f6f08d55 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/body-utils.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from 'vitest'; +import { jsonApiBodyUtils } from '../src/lib/common/jsonapi'; + +describe('jsonApiBodyUtils.pickDefined', () => { + it('drops null and undefined but keeps every other falsy value', () => { + expect( + jsonApiBodyUtils.pickDefined({ + keptFalse: false, + keptZero: 0, + keptEmptyString: '', + droppedNull: null, + droppedUndefined: undefined, + }) + ).toEqual({ keptFalse: false, keptZero: 0, keptEmptyString: '' }); + }); +}); + +describe('jsonApiBodyUtils.omitEmptyObjects', () => { + it('drops empty plain objects and keeps everything else', () => { + expect( + jsonApiBodyUtils.omitEmptyObjects({ + attributes: {}, + relationships: { customer: { data: { type: 'customers', id: '1' } } }, + emptyArray: [], + nullValue: null, + id: '1', + }) + ).toEqual({ + relationships: { customer: { data: { type: 'customers', id: '1' } } }, + emptyArray: [], + nullValue: null, + id: '1', + }); + }); +}); + +describe('jsonApiBodyUtils.buildRels', () => { + it('emits a to-one linkage', () => { + expect(jsonApiBodyUtils.buildRels({ customer: ['customers', '7'] })).toEqual({ + customer: { data: { type: 'customers', id: '7' } }, + }); + }); + + it('wraps a single id in an array when the relationship is to-many', () => { + expect( + jsonApiBodyUtils.buildRels({ userRoles: ['userroles', '3', true] }) + ).toEqual({ userRoles: { data: [{ type: 'userroles', id: '3' }] } }); + }); + + it('emits an array of linkages and drops blank entries', () => { + expect( + jsonApiBodyUtils.buildRels({ groups: ['usergroups', ['1', '', '2']] }) + ).toEqual({ + groups: { data: [{ type: 'usergroups', id: '1' }, { type: 'usergroups', id: '2' }] }, + }); + }); + + it('omits a relationship whose id is absent, blank or an empty list', () => { + expect( + jsonApiBodyUtils.buildRels({ + undefinedId: ['customers', undefined], + nullId: ['customers', null], + blankId: ['customers', ''], + emptyList: ['usergroups', []], + }) + ).toEqual({}); + }); +}); + +describe('jsonApiBodyUtils.parseAdditionalAttributes', () => { + it('returns an empty object when nothing was supplied', () => { + expect(jsonApiBodyUtils.parseAdditionalAttributes(undefined)).toEqual({}); + expect(jsonApiBodyUtils.parseAdditionalAttributes(null)).toEqual({}); + }); + + it('accepts a JSON string as well as an object', () => { + expect(jsonApiBodyUtils.parseAdditionalAttributes('{"myField":"value"}')).toEqual({ + myField: 'value', + }); + expect(jsonApiBodyUtils.parseAdditionalAttributes({ myField: 'value' })).toEqual({ + myField: 'value', + }); + }); + + it('rejects an array and a scalar', () => { + expect(() => jsonApiBodyUtils.parseAdditionalAttributes('[1,2]')).toThrow( + 'Additional Attributes must be a flat JSON object, e.g. {"myField": "value"}.' + ); + expect(() => jsonApiBodyUtils.parseAdditionalAttributes(42)).toThrow( + /must be a flat JSON object/ + ); + }); +}); + +describe('jsonApiBodyUtils.parseAdditionalRelations', () => { + it('accepts a well-formed linkage object', () => { + const value = { myRelation: { data: { type: 'myentities', id: '1' } } }; + expect(jsonApiBodyUtils.parseAdditionalRelations(value)).toEqual(value); + }); + + it('names the offending key when a value is not a linkage object', () => { + expect(() => + jsonApiBodyUtils.parseAdditionalRelations({ myRelation: 'nope' }) + ).toThrow( + 'Additional Relations: "myRelation" must be a JSON:API linkage object with a "data" key, e.g. {"data": {"type": "myentities", "id": "1"}}.' + ); + }); + + it('names the offending key when the "data" wrapper is missing', () => { + expect(() => + jsonApiBodyUtils.parseAdditionalRelations({ myRelation: { type: 'myentities', id: '1' } }) + ).toThrow('Additional Relations: "myRelation" is missing the "data" key.'); + }); + + it('rejects a top-level array', () => { + expect(() => jsonApiBodyUtils.parseAdditionalRelations('[]')).toThrow( + /must be a JSON object/ + ); + }); +}); + +describe('jsonApiBodyUtils.assertUpdateNotEmpty', () => { + it('throws only when both containers are empty', () => { + expect(() => + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes: {}, + relationships: {}, + actionName: 'Update Customer', + }) + ).toThrow('Update Customer: nothing to update. Fill in at least one field or relationship.'); + }); + + it('passes when either container has a key', () => { + expect(() => + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes: { name: 'Acme' }, + relationships: {}, + actionName: 'Update Customer', + }) + ).not.toThrow(); + expect(() => + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes: {}, + relationships: { owner: { data: { type: 'users', id: '1' } } }, + actionName: 'Update Customer', + }) + ).not.toThrow(); + }); +}); diff --git a/packages/pieces/community/orocommerce/test/line-items.test.ts b/packages/pieces/community/orocommerce/test/line-items.test.ts index 0367928b7d99..f06f20abe7e4 100644 --- a/packages/pieces/community/orocommerce/test/line-items.test.ts +++ b/packages/pieces/community/orocommerce/test/line-items.test.ts @@ -42,6 +42,26 @@ describe('lineItemUtils.readRows', () => { ).toThrow(/add at least one row/); }); + it('reports a value that is not a list as such, not as an empty one', () => { + expect(() => + lineItemUtils.readRows({ + value: { lineItems: 'nope' }, + arrayKey: 'lineItems', + displayName, + }) + ).toThrow('Line Items: expected a list of rows, got "nope".'); + expect(() => + lineItemUtils.readRows({ value: { lineItems: 5 }, arrayKey: 'lineItems', displayName }) + ).toThrow('Line Items: expected a list of rows, got 5.'); + expect(() => + lineItemUtils.readRows({ + value: { lineItems: { lineItems: [{ quantity: 1 }] } }, + arrayKey: 'lineItems', + displayName, + }) + ).toThrow('Line Items: expected a list of rows, got an object.'); + }); + it('rejects non-object rows and reports a 1-based index', () => { expect(() => lineItemUtils.readRows({ @@ -85,6 +105,22 @@ describe('lineItemUtils.requiredNumber', () => { ).toThrow(/must be a number/); }); + it('rejects the non-decimal literals Number() would happily parse', () => { + for (const value of ['0x10', '0b101', '0o17', '1_000', '1,000', '$5', '5%']) { + expect(() => + lineItemUtils.requiredNumber({ ...base, row: { quantity: value } }) + ).toThrow(`must be a number, got "${value}"`); + } + }); + + it('still accepts the decimal forms a user may reasonably type', () => { + expect(lineItemUtils.requiredNumber({ ...base, row: { quantity: '+5' } })).toBe(5); + expect(lineItemUtils.requiredNumber({ ...base, row: { quantity: '-5' } })).toBe(-5); + expect(lineItemUtils.requiredNumber({ ...base, row: { quantity: '.5' } })).toBe(0.5); + expect(lineItemUtils.requiredNumber({ ...base, row: { quantity: '5.' } })).toBe(5); + expect(lineItemUtils.requiredNumber({ ...base, row: { quantity: '1e3' } })).toBe(1000); + }); + it('rejects a boolean that Number() would silently turn into 1', () => { expect(() => lineItemUtils.requiredNumber({ ...base, row: { quantity: true } }) @@ -224,6 +260,20 @@ describe('lineItemUtils.assertSumMatches', () => { ).not.toThrow(); }); + it('accepts a half-cent total at zero tolerance, where value * 100 rounds down', () => { + for (const [row, total] of [[1.005, 1.01], [0.145, 0.15], [8.165, 8.17]] as const) { + expect(() => + lineItemUtils.assertSumMatches({ ...params, rows: [{ rowTotal: row }], total }) + ).not.toThrow(); + } + }); + + it('still rejects a real discrepancy at zero tolerance', () => { + expect(() => + lineItemUtils.assertSumMatches({ ...params, rows: [{ rowTotal: 10.005 }], total: 10 }) + ).toThrow(/"Total Amount" is 10, but the sum of "Row Total"/); + }); + it('rejects a non-numeric total', () => { expect(() => lineItemUtils.assertSumMatches({ ...params, rows: [{ rowTotal: 1 }], total: 'x' }) diff --git a/packages/pieces/community/orocommerce/tools/check-i18n.mjs b/packages/pieces/community/orocommerce/tools/check-i18n.mjs index 9d9d706a4bda..e0e32be985e2 100644 --- a/packages/pieces/community/orocommerce/tools/check-i18n.mjs +++ b/packages/pieces/community/orocommerce/tools/check-i18n.mjs @@ -102,7 +102,12 @@ async function loadPiece({ modulePath }) { async function readJson({ filePath }) { const content = await readFile(filePath, 'utf8'); - const parsed = JSON.parse(content); + let parsed; + try { + parsed = JSON.parse(content); + } catch (error) { + throw new Error(`${filePath} is not valid JSON: ${error.message}`); + } if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { throw new Error(`${filePath} must contain a JSON object.`); } @@ -195,9 +200,12 @@ async function run() { reconciled[key] = existing[key] ?? expected[key]; } await writeJson({ filePath: localePath, value: reconciled }); - const added = difference({ from: expectedKeys, without: new Set(Object.keys(existing)) }).length; - const removed = difference({ from: new Set(Object.keys(existing)), without: expectedKeys }).length; - console.log(`reconciled src/i18n/${file}: +${added} seeded, -${removed} stale`); + const added = difference({ from: expectedKeys, without: new Set(Object.keys(existing)) }); + const removed = difference({ from: new Set(Object.keys(existing)), without: expectedKeys }); + console.log(`reconciled src/i18n/${file}: +${added.length} seeded, -${removed.length} stale`); + if (removed.length > 0) { + console.log(` ${reportList({ label: 'Dropped, translation lost', keys: removed })}`); + } continue; } diff --git a/packages/pieces/community/orocommerce/vitest.config.ts b/packages/pieces/community/orocommerce/vitest.config.ts index f520fc141133..ba8ade4a1780 100644 --- a/packages/pieces/community/orocommerce/vitest.config.ts +++ b/packages/pieces/community/orocommerce/vitest.config.ts @@ -10,7 +10,6 @@ export default defineConfig({ }, resolve: { alias: { - '@activepieces/shared': path.resolve(repoRoot, 'packages/core/shared/src/index.ts'), '@activepieces/pieces-framework': path.resolve(repoRoot, 'packages/pieces/framework/src/index.ts'), '@activepieces/pieces-common': path.resolve(repoRoot, 'packages/pieces/common/src/index.ts'), }, From df1fadee4fe824cc90be6238031733c8148bd317 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Tue, 18 Aug 2026 12:16:10 +0200 Subject: [PATCH 37/50] ci: build the orocommerce piece once for both its test and i18n jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The i18n gate was added as its own background `turbo run` next to the test one. Both tasks declare dependsOn: ["build"], and turbo only serialises tasks within a single invocation — across two processes nothing stops piece-orocommerce#build running twice at once against the same dist/, which is the directory check-i18n.mjs imports the built piece from. In practice the earlier "Build changed pieces projects" step warms the cache and both runs hit it, so this has not bitten. But that step is conditional on the piece appearing in the changed-pieces filter, and a PR that touches only tools/check-i18n.mjs, turbo.json or ci.yml does not put it there — so the race is reachable exactly on the PRs that change this tooling. Running both tasks in one invocation lets turbo own the graph. Confirmed with --dry=json: piece-orocommerce#build is scheduled once, and packages that do not define i18n:check resolve to rather than failing. Also gave i18n:write a turbo task. i18n:check gets a fresh dist/ because turbo builds first; i18n:write was only an npm script, so editing a prop description and running it reconciled all six files against the previous build and then failed CI on drift. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 5 +---- turbo.json | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b59a0731922c..12debadcff81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,10 +149,7 @@ jobs: set -euo pipefail pids=() - npx turbo run test --filter=@activepieces/engine --filter=@activepieces/shared --filter=@activepieces/sandbox --filter=@activepieces/ai-providers --filter=@activepieces/pieces-framework --filter=@activepieces/piece-orocommerce & - pids+=($!) - - npx turbo run i18n:check --filter=@activepieces/piece-orocommerce & + npx turbo run test i18n:check --filter=@activepieces/engine --filter=@activepieces/shared --filter=@activepieces/sandbox --filter=@activepieces/ai-providers --filter=@activepieces/pieces-framework --filter=@activepieces/piece-orocommerce & pids+=($!) npx turbo run test-ce test-ee test-cloud check-migrations --filter=api & diff --git a/turbo.json b/turbo.json index 8648366c5cbf..642aad143780 100644 --- a/turbo.json +++ b/turbo.json @@ -118,6 +118,11 @@ "inputs": ["src/**", "tools/**", "package.json"], "outputs": [] }, + "i18n:write": { + "dependsOn": ["build"], + "cache": false, + "outputs": [] + }, "test-ce": { "dependsOn": ["build"], "cache": false From ba576d5765e3466fc5f5065443a8f76b57c4e572 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Wed, 19 Aug 2026 10:56:53 +0200 Subject: [PATCH 38/50] feat(orocommerce): verify the signature on webhook deliveries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trigger returned [context.payload.body] with no verification at all, so anyone who learned the webhook URL could inject fabricated events into a flow. Oro can sign deliveries; the connector simply never asked it to. onEnable now generates a 32-byte secret, sends it as the webhook's `secret` attribute at create, and keeps the only usable copy in the flow-scoped store. run() recomputes hash_hmac('sha256', rawBody, secret) and compares it to the `Webhook-Signature` header before returning anything. Verification is keyed off "this trigger stored a secret", not off the header being present: Oro's addSignature returns early and silently when a webhook has no secret, so trusting header presence would let an attacker skip the check by omitting the header. Entries written before this change carry no secret and keep running unverified — that absence is the whole migration. It verifies payload.rawBody, never a re-serialized payload.body. The server captures rawBody as a UTF-8 string for the string-parsed content types, which is what Oro sends; JSON round-tripping reorders keys and the digest would never match. A missing rawBody with a secret stored is a rejection, not a fallback. A rejected delivery is logged with console.warn and returns [] — no run is created, the caller still gets its 200, and no secret material reaches the log. The success path still returns [context.payload.body]; returning the whole payload would change the output schema of every existing flow. The comparison is the verifyHmacAuth shape from the webhook core piece — explicit length check, then timingSafeEqual — copied rather than imported because pieces may not depend on each other. Not the attio shape, which has no length check and throws on a malformed header. Two compensating deletes: - onEnable creates the webhook before it can store the secret. A failing store.put would leave a live webhook whose secret is unrecoverable and unchangeable, so every delivery would be discarded forever and the only recovery would be disable/enable. The put is wrapped; on failure the webhook is deleted and the original error rethrown. - Republishing a flow calls triggerSourceService.enable without ever calling disable (flow.service.ts -> flow-service-side-effects.ts -> trigger-source-service.ts), so onEnable can run with no matching onDisable. It now deletes a leftover registration found in the store before creating a replacement; the store is keyed by flowId, not flow version, so that entry is still visible. The `secret` attribute needs OroCommerce 6.1, so signing is behind a "Sign webhook deliveries" checkbox defaulting to true. This is a create call, not an update, so there is no "leave unchanged" semantic and the default is safe. Turning it off generates and sends nothing and skips verification. If Oro rejects the create because of the attribute the error surfaces as-is rather than silently retrying unsigned. node:crypto only, no new dependency. Co-Authored-By: Claude Opus 5 (1M context) --- .../pieces/community/orocommerce/README.md | 44 +++ .../community/orocommerce/src/i18n/de.json | 4 +- .../community/orocommerce/src/i18n/fr.json | 4 +- .../community/orocommerce/src/i18n/nl.json | 4 +- .../community/orocommerce/src/i18n/pl.json | 4 +- .../orocommerce/src/i18n/translation.json | 4 +- .../community/orocommerce/src/i18n/uk.json | 4 +- .../src/lib/triggers/webhook-topic-trigger.ts | 143 ++++++-- .../test/webhook-signature.test.ts | 331 ++++++++++++++++++ 9 files changed, 516 insertions(+), 26 deletions(-) create mode 100644 packages/pieces/community/orocommerce/test/webhook-signature.test.ts diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 92ce06f6f9a3..85110ab48902 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -52,6 +52,15 @@ to-many relationship is a **full replace** — see *Update actions replace to-ma dropdown lists only the topics your connection can read. Enabling the trigger registers the webhook in Oro; disabling it removes the registration. +**Sign webhook deliveries** is on by default. Enabling the trigger then generates a secret, hands it +to Oro at registration, and every later delivery must carry a matching `Webhook-Signature` header or +it is discarded without starting a run. This needs **OroCommerce 6.1 or newer** — older versions +reject the secret and the registration fails, so turn the checkbox off for them. With signing off, +anyone who learns the webhook URL can start the flow with a payload of their choosing. + +The secret cannot be read back or changed after registration. To rotate it, disable and re-enable +the trigger, which deletes the old webhook and registers a new one. + ## Reporting issues Open an issue at with your OroCommerce version, the @@ -235,6 +244,41 @@ record — and is not used here. `attributes: {}` / `relationships: {}` object from `data` before sending, so action code can build those containers unconditionally without emitting empty ones on the wire. +## Webhook deliveries are verified against the raw body + +Oro signs the exact bytes it sends: `hash_hmac('sha256', rawBody, secret)`, hex, in the +`Webhook-Signature` header. `run` therefore verifies `context.payload.rawBody`, never a +re-serialized `context.payload.body` — JSON round-tripping reorders keys and changes whitespace, and +the digest would never match. + +Verification keys off *the trigger having stored a secret*, not off the header being present. Oro +sends no signature at all when a webhook has no secret configured, so trusting header presence would +let an attacker skip verification by omitting the header. A store entry written before this feature +existed has no `secret`, and that absence means "keep running unverified" — no migration, no version +field. + +`onEnable` creates the Oro webhook before it can store the secret, so a failing `store.put` would +otherwise leave a live webhook whose secret is unrecoverable and whose every delivery is discarded +forever. The `put` is wrapped: on failure the webhook is deleted (errors from that delete are +swallowed) and the original error is rethrown. + +`onEnable` also deletes a leftover registration found in the store before creating a replacement. +Republishing a flow calls `triggerSourceService.enable` without ever calling `disable` +(`flow.service.ts` → `flow-service-side-effects.ts` → `trigger-source-service.ts`), so `onEnable` +really does run without a matching `onDisable`; without that cleanup every republish would leak an +Oro webhook whose secret is no longer stored anywhere. The store is keyed by `flowId`, not by flow +version (`StoreScope.FLOW` in `packages/server/engine/src/lib/piece-context/store.ts`), so the +leftover entry is still visible at that point. + +A rejected delivery is logged with `console.warn` and returns `[]` — no run is created, the caller +still gets its 200, and no secret material reaches the log. The success path returns +`[context.payload.body]`; returning the whole payload would change the output schema of every +existing flow. + +The HMAC comparison mirrors `verifyHmacAuth` in +`packages/pieces/core/webhook/src/lib/triggers/catch-hook.ts` — explicit length check, then +`timingSafeEqual`. It is copied rather than imported: pieces may not depend on each other. + ## Local development ```bash diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json index 2f8b1e464a13..f868bac8685e 100644 --- a/packages/pieces/community/orocommerce/src/i18n/de.json +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -253,5 +253,7 @@ "Oro Webhook Event": "Oro Webhook Event", "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", "Topic": "Topic", - "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" + "Sign webhook deliveries": "Sign webhook deliveries", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." } diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json index 4c6ef423bd9a..3d2ce83b0a25 100644 --- a/packages/pieces/community/orocommerce/src/i18n/fr.json +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -253,5 +253,7 @@ "Oro Webhook Event": "Oro Webhook Event", "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", "Topic": "Topic", - "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" + "Sign webhook deliveries": "Sign webhook deliveries", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." } diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json index 4eb3295ac5f5..e4fc6eda835f 100644 --- a/packages/pieces/community/orocommerce/src/i18n/nl.json +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -253,5 +253,7 @@ "Oro Webhook Event": "Oro Webhook Event", "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", "Topic": "Topic", - "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" + "Sign webhook deliveries": "Sign webhook deliveries", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." } diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json index c62458c9c113..1197b0aa6642 100644 --- a/packages/pieces/community/orocommerce/src/i18n/pl.json +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -253,5 +253,7 @@ "Oro Webhook Event": "Oro Webhook Event", "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", "Topic": "Topic", - "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" + "Sign webhook deliveries": "Sign webhook deliveries", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." } diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json index c3f28eac6796..f71fdf29d6c7 100644 --- a/packages/pieces/community/orocommerce/src/i18n/translation.json +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -253,5 +253,7 @@ "Oro Webhook Event": "Oro Webhook Event", "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", "Topic": "Topic", - "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" + "Sign webhook deliveries": "Sign webhook deliveries", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." } diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json index 5f5dff15e675..8b995bad81dd 100644 --- a/packages/pieces/community/orocommerce/src/i18n/uk.json +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -253,5 +253,7 @@ "Oro Webhook Event": "Oro Webhook Event", "Trigger when a selected webhook event is raised": "Trigger when a selected webhook event is raised", "Topic": "Topic", - "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown" + "Sign webhook deliveries": "Sign webhook deliveries", + "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." } diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index c1bddca1f9a6..38e50053b8bc 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -1,3 +1,5 @@ +import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; + import { createTrigger, Property, @@ -43,11 +45,30 @@ export const oroWebhookTopicTrigger = createTrigger({ }; }, }), + signDeliveries: Property.Checkbox({ + displayName: 'Sign webhook deliveries', + description: + 'Register the webhook with a shared secret and discard deliveries whose "Webhook-Signature" header does not match the body. ' + + 'Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. ' + + 'Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.', + required: false, + defaultValue: true, + }), }, type: TriggerStrategy.WEBHOOK, sampleData: {}, async onEnable(context) { + const staleInfo = await context.store.get('webhookInfo'); + if (staleInfo !== null && staleInfo !== undefined) { + await discardWebhook({ auth: context.auth, webhookId: staleInfo.webhookId }); + } + + const secret = + context.propsValue.signDeliveries === false + ? undefined + : randomBytes(32).toString('hex'); + const response = await oroApiCall({ method: HttpMethod.POST, resourceUri: 'webhooks', @@ -58,6 +79,7 @@ export const oroWebhookTopicTrigger = createTrigger({ attributes: { enabled: true, notificationUrl: context.webhookUrl, + ...(secret === undefined ? {} : { secret }), }, relationships: { topic: { @@ -82,43 +104,124 @@ export const oroWebhookTopicTrigger = createTrigger({ throw new Error('OroCommerce webhook registration failed: no webhook ID returned. Check your connection and permissions.'); } - await context.store.put('webhookInfo', { - webhookId, - topic: context.propsValue.topic, - }); + try { + await context.store.put('webhookInfo', { + webhookId, + topic: context.propsValue.topic, + ...(secret === undefined ? {} : { secret }), + }); + } catch (error: unknown) { + await discardWebhook({ auth: context.auth, webhookId }); + throw error; + } }, async onDisable(context) { const webhookInfo = await context.store.get('webhookInfo'); if (webhookInfo !== null && webhookInfo !== undefined) { - try { - await oroApiCall({ - method: HttpMethod.DELETE, - resourceUri: `webhooks/${webhookInfo.webhookId}`, - auth: context.auth, - throwOriginalError: true, - }); - } catch (error: unknown) { - const alreadyGone = - error instanceof HttpError && - [401, 403, 404].includes(error.response.status); - - if (!alreadyGone) { - throw new Error(formatError({ error })); - } - } + await deleteWebhook({ auth: context.auth, webhookId: webhookInfo.webhookId }); await context.store.delete('webhookInfo'); } }, async run(context) { + const webhookInfo = await context.store.get('webhookInfo'); + const secret = webhookInfo?.secret; + + if (secret === undefined || secret === null) { + return [context.payload.body]; + } + + const rejection = findSignatureRejection({ + headers: context.payload.headers, + rawBody: context.payload.rawBody, + secret, + }); + + if (rejection !== undefined) { + console.warn( + `OroCommerce webhook delivery discarded (flow ${context.flows.current.id}, step "${context.step.name}"): ${rejection}.` + ); + return []; + } + return [context.payload.body]; }, }); +async function deleteWebhook({ + auth, + webhookId, +}: { + auth: OroAuth; + webhookId: string; +}): Promise { + try { + await oroApiCall({ + method: HttpMethod.DELETE, + resourceUri: `webhooks/${webhookId}`, + auth, + throwOriginalError: true, + }); + } catch (error: unknown) { + const alreadyGone = + error instanceof HttpError && + [401, 403, 404].includes(error.response.status); + + if (!alreadyGone) { + throw new Error(formatError({ error })); + } + } +} + +async function discardWebhook({ + auth, + webhookId, +}: { + auth: OroAuth; + webhookId: string; +}): Promise { + try { + await deleteWebhook({ auth, webhookId }); + } catch { + return; + } +} + +function findSignatureRejection({ + headers, + rawBody, + secret, +}: { + headers: Record | undefined; + rawBody: unknown; + secret: string; +}): string | undefined { + const signature = headers?.['webhook-signature']; + if (!signature) { + return 'the Webhook-Signature header is missing'; + } + + if (typeof rawBody !== 'string' && !Buffer.isBuffer(rawBody)) { + return 'the raw request body was not captured, so the signature cannot be checked'; + } + + const expected = Buffer.from( + createHmac('sha256', secret).update(rawBody).digest('hex') + ); + const received = Buffer.from(signature); + + if (expected.length !== received.length || !timingSafeEqual(expected, received)) { + return 'the Webhook-Signature header does not match the delivered body'; + } + + return undefined; +} + interface WebhookInformation { webhookId: string; topic: string; + secret?: string; } diff --git a/packages/pieces/community/orocommerce/test/webhook-signature.test.ts b/packages/pieces/community/orocommerce/test/webhook-signature.test.ts new file mode 100644 index 000000000000..de79112c2ea2 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/webhook-signature.test.ts @@ -0,0 +1,331 @@ +import { createHmac } from 'node:crypto'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { oroApiCall } from '../src/lib/common'; +import { oroWebhookTopicTrigger } from '../src/lib/triggers/webhook-topic-trigger'; + +vi.mock('../src/lib/common', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, oroApiCall: vi.fn() }; +}); + +type EnableContext = Parameters[0]; +type RunContext = Parameters[0]; + +const TOPIC = 'oro.customer.created'; +const SECRET = 'a'.repeat(64); +const RAW_BODY = '{"event":"oro.customer.created","id":42}'; +const BODY = { event: 'oro.customer.created', id: 42 }; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function sign({ rawBody, secret }: { rawBody: string; secret: string }): string { + return createHmac('sha256', secret).update(rawBody).digest('hex'); +} + +function createStore(entry?: unknown) { + const values = new Map(); + if (entry !== undefined) { + values.set('webhookInfo', entry); + } + + return { + values, + put: vi.fn(async (key: string, value: unknown) => { + values.set(key, value); + return value; + }), + get: vi.fn(async (key: string) => values.get(key) ?? null), + delete: vi.fn(async (key: string) => { + values.delete(key); + }), + }; +} + +function createRunContext({ + store, + headers, + rawBody, +}: { + store: ReturnType; + headers?: Record; + rawBody?: unknown; +}): RunContext { + return { + store, + payload: { + body: BODY, + rawBody, + headers, + queryParams: {}, + }, + propsValue: { topic: TOPIC, signDeliveries: true }, + flows: { current: { id: 'flow-1', version: { id: 'flow-version-1' } } }, + step: { name: 'trigger' }, + } as unknown as RunContext; +} + +function createEnableContext({ + store, + signDeliveries, +}: { + store: ReturnType; + signDeliveries?: boolean; +}): EnableContext { + return { + store, + auth: { + type: 'CUSTOM_AUTH', + props: { + serverUrl: 'https://store.example.com', + adminPrefix: 'admin', + clientId: 'client-id', + clientSecret: 'client-secret', + isInternalInfrastructure: false, + }, + }, + propsValue: { topic: TOPIC, signDeliveries }, + webhookUrl: 'https://activepieces.example.com/webhooks/flow-1', + } as unknown as EnableContext; +} + +function createdAttributes(): Record { + const body = vi.mocked(oroApiCall).mock.calls[0][0].body; + const data = isRecord(body) ? body['data'] : undefined; + const attributes = isRecord(data) ? data['attributes'] : undefined; + if (!isRecord(attributes)) { + throw new Error('the webhook create call carried no attributes object'); + } + return attributes; +} + +function storedInfo(store: ReturnType): Record { + const value = store.values.get('webhookInfo'); + if (!isRecord(value)) { + throw new Error('nothing was stored under webhookInfo'); + } + return value; +} + +describe('the webhook trigger verifies signed deliveries', () => { + beforeEach(() => { + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('accepts a delivery whose signature covers the raw body', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ + store, + headers: { 'webhook-signature': sign({ rawBody: RAW_BODY, secret: SECRET }) }, + rawBody: RAW_BODY, + }) + ); + + expect(result).toStrictEqual([BODY]); + }); + + it('accepts a delivery whose raw body arrived as a buffer', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ + store, + headers: { 'webhook-signature': sign({ rawBody: RAW_BODY, secret: SECRET }) }, + rawBody: Buffer.from(RAW_BODY, 'utf8'), + }) + ); + + expect(result).toStrictEqual([BODY]); + }); + + it('drops a delivery that carries no signature header', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ store, headers: {}, rawBody: RAW_BODY }) + ); + + expect(result).toStrictEqual([]); + }); + + it('drops a delivery signed with the wrong key', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ + store, + headers: { + 'webhook-signature': sign({ rawBody: RAW_BODY, secret: 'b'.repeat(64) }), + }, + rawBody: RAW_BODY, + }) + ); + + expect(result).toStrictEqual([]); + }); + + it('drops a delivery whose signature header is too short to compare', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ + store, + headers: { 'webhook-signature': 'not-a-signature' }, + rawBody: RAW_BODY, + }) + ); + + expect(result).toStrictEqual([]); + }); + + it('drops a delivery that arrived with no headers at all', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ store, rawBody: RAW_BODY }) + ); + + expect(result).toStrictEqual([]); + }); + + it('drops a delivery whose raw body was not captured', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ + store, + headers: { 'webhook-signature': sign({ rawBody: RAW_BODY, secret: SECRET }) }, + }) + ); + + expect(result).toStrictEqual([]); + }); + + it('verifies the bytes Oro sent, not a re-serialized body', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + const reordered = JSON.stringify({ id: 42, event: 'oro.customer.created' }); + + expect(reordered).not.toBe(RAW_BODY); + expect(reordered.length).toBe(RAW_BODY.length); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ + store, + headers: { 'webhook-signature': sign({ rawBody: reordered, secret: SECRET }) }, + rawBody: RAW_BODY, + }) + ); + + expect(result).toStrictEqual([]); + }); + + it('keeps a flow enabled before signing existed running unverified', async () => { + const store = createStore({ webhookId: 'wh-legacy', topic: TOPIC }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ store, headers: {}, rawBody: RAW_BODY }) + ); + + expect(result).toStrictEqual([BODY]); + }); + + it('does not start verifying just because a signature header showed up', async () => { + const store = createStore({ webhookId: 'wh-legacy', topic: TOPIC }); + + const result = await oroWebhookTopicTrigger.run( + createRunContext({ + store, + headers: { 'webhook-signature': 'f'.repeat(64) }, + rawBody: RAW_BODY, + }) + ); + + expect(result).toStrictEqual([BODY]); + }); +}); + +describe('enabling the webhook trigger provisions the signing secret', () => { + beforeEach(() => { + vi.mocked(oroApiCall).mockReset(); + vi.mocked(oroApiCall).mockResolvedValue({ + status: 201, + headers: {}, + body: { data: { id: 'wh-created' } }, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('sends a fresh secret to Oro and keeps the only copy in the store', async () => { + const store = createStore(); + + await oroWebhookTopicTrigger.onEnable(createEnableContext({ store, signDeliveries: true })); + + const secret = createdAttributes()['secret']; + expect(secret).toMatch(/^[0-9a-f]{64}$/); + expect(storedInfo(store)).toStrictEqual({ + webhookId: 'wh-created', + topic: TOPIC, + secret, + }); + }); + + it('signs by default when the flow never set the checkbox', async () => { + const store = createStore(); + + await oroWebhookTopicTrigger.onEnable(createEnableContext({ store })); + + expect(createdAttributes()['secret']).toMatch(/^[0-9a-f]{64}$/); + }); + + it('sends no secret at all when signing is turned off', async () => { + const store = createStore(); + + await oroWebhookTopicTrigger.onEnable(createEnableContext({ store, signDeliveries: false })); + + expect(createdAttributes()).not.toHaveProperty('secret'); + expect(storedInfo(store)).toStrictEqual({ webhookId: 'wh-created', topic: TOPIC }); + }); + + it('removes the webhook it just created when the secret cannot be stored', async () => { + const store = createStore(); + const storeFailure = new Error('store unavailable'); + store.put.mockRejectedValueOnce(storeFailure); + + await expect( + oroWebhookTopicTrigger.onEnable(createEnableContext({ store, signDeliveries: true })) + ).rejects.toBe(storeFailure); + + expect(vi.mocked(oroApiCall)).toHaveBeenCalledTimes(2); + expect(vi.mocked(oroApiCall).mock.calls[1][0]).toMatchObject({ + method: HttpMethod.DELETE, + resourceUri: 'webhooks/wh-created', + }); + }); + + it('drops a leftover registration before creating a replacement', async () => { + const store = createStore({ webhookId: 'wh-stale', topic: TOPIC, secret: SECRET }); + + await oroWebhookTopicTrigger.onEnable(createEnableContext({ store, signDeliveries: true })); + + expect(vi.mocked(oroApiCall).mock.calls[0][0]).toMatchObject({ + method: HttpMethod.DELETE, + resourceUri: 'webhooks/wh-stale', + }); + expect(vi.mocked(oroApiCall).mock.calls[1][0]).toMatchObject({ + method: HttpMethod.POST, + resourceUri: 'webhooks', + }); + expect(storedInfo(store)['webhookId']).toBe('wh-created'); + }); +}); From 5648d4df0bf5f145e66a45db2b1d21d7912224fb Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Wed, 19 Aug 2026 12:29:11 +0200 Subject: [PATCH 39/50] fix(orocommerce): keep the rejection log safe on engines older than 0.71.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review flagged the console.warn in run() as the riskiest line in the file: it read context.flows.current.id and context.step.name, which the framework types guarantee but the runtime only partially does. flows has been on the trigger run context since at least release 0.46.0, but step was only added in d64d7bf8f3 (2025-10-12), first shipped in 0.71.0. On any older engine the plain property access throws TypeError on the rejection path — turning "discard the forged delivery" into "error the delivery", on the security path of all places. Both reads now go through optional chaining with an 'unknown' fallback, and a test runs the rejection path against a context shaped like a pre-0.71.0 engine (no flows, no step) to prove it returns [] instead of throwing. Co-Authored-By: Claude Opus 5 (1M context) --- .../pieces/community/orocommerce/README.md | 6 +++++- .../src/lib/triggers/webhook-topic-trigger.ts | 2 +- .../orocommerce/test/webhook-signature.test.ts | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 85110ab48902..d8e5504fb612 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -271,7 +271,11 @@ version (`StoreScope.FLOW` in `packages/server/engine/src/lib/piece-context/stor leftover entry is still visible at that point. A rejected delivery is logged with `console.warn` and returns `[]` — no run is created, the caller -still gets its 200, and no secret material reaches the log. The success path returns +still gets its 200, and no secret material reaches the log. The log line reads `context.flows` and +`context.step` through optional chaining: `step` only exists on the trigger run context since +Activepieces 0.71.0 (engine commit `d64d7bf8f3`), and a plain property access would turn "discard +the forged delivery" into a thrown `TypeError` on older engines — on the security path, of all +places. The success path returns `[context.payload.body]`; returning the whole payload would change the output schema of every existing flow. diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index 38e50053b8bc..d05d9d77d734 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -142,7 +142,7 @@ export const oroWebhookTopicTrigger = createTrigger({ if (rejection !== undefined) { console.warn( - `OroCommerce webhook delivery discarded (flow ${context.flows.current.id}, step "${context.step.name}"): ${rejection}.` + `OroCommerce webhook delivery discarded (flow ${context.flows?.current?.id ?? 'unknown'}, step "${context.step?.name ?? 'unknown'}"): ${rejection}.` ); return []; } diff --git a/packages/pieces/community/orocommerce/test/webhook-signature.test.ts b/packages/pieces/community/orocommerce/test/webhook-signature.test.ts index de79112c2ea2..6c1459342e9b 100644 --- a/packages/pieces/community/orocommerce/test/webhook-signature.test.ts +++ b/packages/pieces/community/orocommerce/test/webhook-signature.test.ts @@ -209,6 +209,24 @@ describe('the webhook trigger verifies signed deliveries', () => { expect(result).toStrictEqual([]); }); + it('still discards cleanly on an engine that predates flows/step in the context', async () => { + const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); + const legacyEngineContext = { + store, + payload: { + body: BODY, + rawBody: RAW_BODY, + headers: {}, + queryParams: {}, + }, + propsValue: { topic: TOPIC, signDeliveries: true }, + } as unknown as RunContext; + + const result = await oroWebhookTopicTrigger.run(legacyEngineContext); + + expect(result).toStrictEqual([]); + }); + it('verifies the bytes Oro sent, not a re-serialized body', async () => { const store = createStore({ webhookId: 'wh-1', topic: TOPIC, secret: SECRET }); const reordered = JSON.stringify({ id: 42, event: 'oro.customer.created' }); From da1f39576903b0d17af0ecc54f79122a4ce5bb59 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Wed, 19 Aug 2026 12:29:49 +0200 Subject: [PATCH 40/50] docs: record the OroCommerce 6.1 signing floor as a functional breaking change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reviewer overruled the PR's "not breaking" self-assessment: the template defines functional as "default/limit/behaviour change", and a default-on behaviour whose first failure appears at the first re-enable after upgrade reads, from the user's chair, as "the upgrade broke my trigger". The entry sits under 0.87.0 because that is this tree's version; move it if the fork's release numbering says otherwise. Placed with the label so breaking-change-check's invariant (label and docs entry travel together) holds if this is ever retargeted upstream — the job itself is gated to github.repository == activepieces/activepieces and does not run on the fork. Co-Authored-By: Claude Opus 5 (1M context) --- docs/install/reference/breaking-changes.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/install/reference/breaking-changes.mdx b/docs/install/reference/breaking-changes.mdx index 3d23fad9b6a8..54a5055f95f8 100644 --- a/docs/install/reference/breaking-changes.mdx +++ b/docs/install/reference/breaking-changes.mdx @@ -9,6 +9,11 @@ icon: "hammer" ### What has changed? +#### The OroCommerce piece signs webhook deliveries by default + +The Oro Webhook Event trigger now registers its webhook with a signing secret and discards deliveries whose `Webhook-Signature` header does not match the delivered body. The secret attribute requires OroCommerce 6.1 or newer — on older versions, enabling, re-enabling or republishing a flow with this trigger fails at webhook registration until the trigger's **Sign webhook deliveries** checkbox is turned off. Flows that are already enabled keep running unverified until they are next re-enabled. + + #### Plans and credits are now managed by our billing service Two license-key endpoints are removed: `GET /v1/license-keys/:licenseKey` and `POST /v1/license-keys/verify`. Applying a key is now `POST /v1/platform-billing/activate`. @@ -25,6 +30,7 @@ When a platform has no credits left, new production runs are recorded with the ` In the AI piece, picking the Activepieces-provided AI now lists only the named tiers (Fast, Expert, Heavy) rather than the full upstream model catalogue. Existing steps keep running on the model they already have, but that model no longer appears in the dropdown, so re-saving the step moves it onto one of the tiers. ### Do you need to take action? +- Only if a flow uses the Oro Webhook Event trigger against an OroCommerce instance older than 6.1. Untick **Sign webhook deliveries** on the trigger before re-enabling or republishing that flow; existing enabled flows keep working until then. - Only if you run the Enterprise edition without a license key. Enter your key so your contracted limits apply instead of the free plan's. - Only if you call `GET /v1/license-keys/:licenseKey` or `POST /v1/license-keys/verify`. Both are removed — use `POST /v1/platform-billing/activate` instead. - Only if you read `SHOW_BILLING_PAGE`, `CAN_BUY_ACTIVE_FLOWS`, `CAN_BUY_AI_CREDITS` or `SHOW_BILLING_LIMITS_ON_SIDEBAR` from `GET /v1/flags`. They no longer exist. From c4f8bfe445abe9a079c80cbbf39bc1d8edafcbb5 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 27 Aug 2026 09:05:01 +0200 Subject: [PATCH 41/50] ci: run the orocommerce piece checks on the fork MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every job in ci.yml is guarded by if: github.repository == 'activepieces/activepieces' so on oroinc/activepieces lint, main and tool-search-postgres skip unconditionally. The same guard is on breaking-change-check.yml, pr-size.yml and setup-environment.yml. Validate PR title is the only check that has ever run on a fork PR — every other check on #4 and #5 reports SKIPPED, including the i18n gate and the piece tests added in 7ca5361063 and 36372f456b. Neither has executed once. Relaxing the upstream guard was the other option and is worse: it edits a file that conflicts on every sync, and the jobs behind it want TURBO_CACHE_S3_* secrets the fork does not have (gh secret list is empty), plus a full-monorepo build the fork has no reason to pay for. So this is a fork-owned workflow instead, guarded the other way round (!= upstream) so it stays inert if the piece is ever contributed upstream, and scoped to the one package the fork owns. No paths filter: the piece breaks from upstream merges that touch the framework, not only from edits under its own directory, and those merges do not touch it. Verified the gate fails as intended, not just passes — a stale key in translation.json, a key dropped from de.json, and a changed piece description each exit 1 with the offending keys named. Also confirmed the 113-commit merge did not move anything check-i18n.mjs hardcodes: pathsToValuesToTranslate is the same 19 paths, MAX_KEY_LENGTH_FOR_CORWDIN is still 512, LocalesEnum is still the same 10, and the CLI generator's getPropertyValue is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/oro-ci.yml | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/oro-ci.yml diff --git a/.github/workflows/oro-ci.yml b/.github/workflows/oro-ci.yml new file mode 100644 index 000000000000..2b4b383981f2 --- /dev/null +++ b/.github/workflows/oro-ci.yml @@ -0,0 +1,38 @@ +name: Oro CI + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: oro-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + orocommerce-piece: + name: orocommerce piece + runs-on: ubuntu-latest + if: github.repository != 'activepieces/activepieces' + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: ${{ runner.os }}-bun- + + - run: bun install + + - name: Build, lint, test and check translations + run: npx turbo run build lint test i18n:check --filter=@activepieces/piece-orocommerce From 2cafcf13d460e3bd036433b0ee2584e353c1ff84 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 27 Aug 2026 15:37:07 +0200 Subject: [PATCH 42/50] fix(orocommerce): stop the update actions from disabling the record they patch Update User and Update Customer User sent enabled: false on every run, so "Update User, change the last name" also locked the account out. Verified against a live Oro 7.1 instance: oro_user 6 went Pace|t -> SmokeTest-V1|f and oro_customer_user 1 went Cole|t -> SmokeTest-V2|f, each from a step whose Enabled box was never touched. The second one is a storefront login. The cause is not in this piece. The builder seeds an unset checkbox with property.defaultValue ?? false (web/src/features/pieces/utils/form-utils.tsx) and persists it into the step input, and checkboxProcessor passes false through - Checkbox is the one property type whose empty form value is not normalised to undefined the way textProcessor and numberProcessor normalise theirs. So `p.enabled ?? undefined` was false, pickDefined kept it, and assertUpdateNotEmpty could never fire for either action: there was always one attribute to send. Both flags, and Confirmed on the customer user, are now a three-state StaticDropdown defaulting to "Leave unchanged", the shape campaign-monitor's update-subscriber-details already uses. A defaultValue on the checkbox was the other option and is worse: true would unconditionally enable instead. readBooleanUpdate ignores a boolean false, which is what a step saved by the checkbox version of the prop still holds. There it means "untouched" at least as often as it means "disable", and the two are indistinguishable, so the safe reading is to leave the record alone - an existing flow stops disabling its target rather than carrying the defect forward. Set the dropdown to "No" to disable deliberately. Re-verified on the same rig, three runs through the flow-operations API: untouched Enabled plus a new last name leaves enabled = t; a step input holding the old boolean false also leaves enabled = t; "No" still sets enabled = f. And an update whose only field is an untouched flag now reports "nothing to update" instead of quietly disabling the account. i18n regenerated: +3 keys for the option labels, none dropped, so no existing translation is lost. Co-Authored-By: Claude Opus 5 (1M context) --- .../pieces/community/orocommerce/README.md | 24 ++++++----- .../community/orocommerce/src/i18n/de.json | 3 ++ .../community/orocommerce/src/i18n/fr.json | 3 ++ .../community/orocommerce/src/i18n/nl.json | 3 ++ .../community/orocommerce/src/i18n/pl.json | 3 ++ .../orocommerce/src/i18n/translation.json | 3 ++ .../community/orocommerce/src/i18n/uk.json | 3 ++ .../src/lib/actions/update-customer-user.ts | 12 +++--- .../src/lib/actions/update-user.ts | 7 ++-- .../orocommerce/src/lib/common/props.ts | 41 +++++++++++++++++++ .../orocommerce/test/action-guards.test.ts | 37 +++++++++++++++++ .../orocommerce/test/boolean-update.test.ts | 37 +++++++++++++++++ 12 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 packages/pieces/community/orocommerce/test/boolean-update.test.ts diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 92ce06f6f9a3..ba6bd59bbce3 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -336,18 +336,22 @@ client secret always come from the connection. iterates `LocalesEnum` (`packages/core/utils/src/lib/locale.ts`), which has no Polish or Ukrainian. The gate keeps them in sync so they are ready if those locales are added, and `i18n:check` prints a warning for each. -- **An untouched `Property.Checkbox` arrives as `false`, not `undefined`.** The builder seeds an - unset checkbox with `property.defaultValue ?? false` +- **An untouched `Property.Checkbox` arrives as `false`, not `undefined`, so no update action may use + one.** The builder seeds an unset checkbox with `property.defaultValue ?? false` (`packages/web/src/features/pieces/utils/form-utils.tsx`) and persists it into the step input, and `checkboxProcessor` passes `false` through — it is the one property type whose "empty" form value is - not normalised to `undefined` the way `textProcessor` and `numberProcessor` normalise theirs. So - `p.enabled ?? undefined` is `false`, `pickDefined` keeps it, and `update-user` / `update-customer-user` - send `enabled: false` on every call while `assertUpdateNotEmpty` can never fire for them. Fixing it - means a three-state prop (a `Property.StaticDropdown` with a "leave unchanged" option, as in - `campaign-monitor/src/lib/actions/update-subscriber-details.ts`), which changes the props and so - needs an i18n regeneration — do not paper over it with a `defaultValue`, since `true` would - unconditionally *enable* instead. Hand-written `propsValue` in `test/action-guards.test.ts` does not - reproduce this; a builder-shaped case must include `enabled: false`. + not normalised to `undefined` the way `textProcessor` and `numberProcessor` normalise theirs. A + checkbox therefore cannot say "leave this alone": `update-user` and `update-customer-user` used to + send `enabled: false` on every call and disable the account they were only asked to rename, and + `assertUpdateNotEmpty` could never fire for them. Those flags are now `booleanUpdateDropdown` in + `src/lib/common/props.ts` — a three-state `Property.StaticDropdown` defaulting to *Leave unchanged*, + as in `campaign-monitor/src/lib/actions/update-subscriber-details.ts` — read back with + `readBooleanUpdate`, which also ignores the `false` a step saved by the checkbox version still + holds, so an existing flow stops disabling its target. Give any new boolean on an update action the + same treatment. A `defaultValue` is not a fix: `true` would unconditionally *enable* instead. Create + actions keep their checkboxes, where an unchecked box and `false` mean the same thing. Note that a + hand-written `propsValue` in `test/action-guards.test.ts` does not reproduce the builder's `false` — + a case that stands in for a saved step has to pass it explicitly. - Line-item input is validated through `lineItemUtils` (`src/lib/common/line-items.ts`), not bare `Number()`. `Number(undefined)` is `NaN` and `JSON.stringify` serialises `NaN` as `null`, so an unvalidated missing quantity used to reach Oro as `null` with no error. Route any new line-item diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json index 2f8b1e464a13..b6c88243248d 100644 --- a/packages/pieces/community/orocommerce/src/i18n/de.json +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -240,6 +240,9 @@ "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", "GET": "GET", "POST": "POST", "PATCH": "PATCH", diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json index 4c6ef423bd9a..f372c26cff17 100644 --- a/packages/pieces/community/orocommerce/src/i18n/fr.json +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -240,6 +240,9 @@ "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", "GET": "GET", "POST": "POST", "PATCH": "PATCH", diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json index 4eb3295ac5f5..0fae6aa6194d 100644 --- a/packages/pieces/community/orocommerce/src/i18n/nl.json +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -240,6 +240,9 @@ "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", "GET": "GET", "POST": "POST", "PATCH": "PATCH", diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json index c62458c9c113..885a4501cc49 100644 --- a/packages/pieces/community/orocommerce/src/i18n/pl.json +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -240,6 +240,9 @@ "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", "GET": "GET", "POST": "POST", "PATCH": "PATCH", diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json index c3f28eac6796..f409130ee79d 100644 --- a/packages/pieces/community/orocommerce/src/i18n/translation.json +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -240,6 +240,9 @@ "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", "GET": "GET", "POST": "POST", "PATCH": "PATCH", diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json index 5f5dff15e675..35960a900274 100644 --- a/packages/pieces/community/orocommerce/src/i18n/uk.json +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -240,6 +240,9 @@ "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}": "Wins over anything detected in Attributes. Example: {\"customer\":{\"type\":\"customers\",\"id\":\"42\"}}", "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".": "Extra resources to embed. Forwarded automatically when Attributes already has \"included\".", "The \"body\" output of the API Call action. Needs a top-level \"data\" key.": "The \"body\" output of the API Call action. Needs a top-level \"data\" key.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", "GET": "GET", "POST": "POST", "PATCH": "PATCH", diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index aa6b7ba1d169..d9f28123944c 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -8,6 +8,8 @@ import { organizationDropdown, userDropdown, websiteDropdown, + booleanUpdateDropdown, + readBooleanUpdate, additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, @@ -65,15 +67,13 @@ export const updateCustomerUserAction = createAction({ required: false, }), - enabled: Property.Checkbox({ + enabled: booleanUpdateDropdown({ displayName: 'Enabled', description: 'Enable or disable the storefront account.', - required: false, }), - confirmed: Property.Checkbox({ + confirmed: booleanUpdateDropdown({ displayName: 'Confirmed', description: 'Whether the user has completed email confirmation.', - required: false, }), birthday: Property.ShortText({ displayName: 'Birthday', @@ -109,8 +109,8 @@ export const updateCustomerUserAction = createAction({ firstName: p.firstName, lastName: p.lastName, password: p.password, - enabled: p.enabled ?? undefined, - confirmed: p.confirmed ?? undefined, + enabled: readBooleanUpdate(p.enabled), + confirmed: readBooleanUpdate(p.confirmed), namePrefix: p.namePrefix, middleName: p.middleName, nameSuffix: p.nameSuffix, diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts index 0a626de588d3..397b22ed130e 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -10,6 +10,8 @@ import { userRolesMultiDropdown, userGroupsMultiDropdown, userAuthStatusDropdown, + booleanUpdateDropdown, + readBooleanUpdate, additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, @@ -84,10 +86,9 @@ export const updateUserAction = createAction({ description: 'Birth date in YYYY-MM-DD format.', required: false, }), - enabled: Property.Checkbox({ + enabled: booleanUpdateDropdown({ displayName: 'Enabled', description: 'Enable or disable the user account.', - required: false, }), // -- Relationships --------------------------------------------------------- @@ -117,7 +118,7 @@ export const updateUserAction = createAction({ password: p.password, firstName: p.firstName, lastName: p.lastName, - enabled: p.enabled ?? undefined, + enabled: readBooleanUpdate(p.enabled), namePrefix: p.namePrefix, middleName: p.middleName, nameSuffix: p.nameSuffix, diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index b44c74940c97..0d2bfddf9625 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -704,6 +704,45 @@ export const customerUserRolesMultiDropdown = makeMultiSelectDropdown({ labelFn: attrLabel('label', 'role'), }); +// --- Boolean flags on update actions ------------------------------------------ + +// An untouched Property.Checkbox arrives as `false`, not `undefined` (see the README gotcha), so a +// checkbox cannot say "leave this alone" — an update action built on one sends the flag off on every +// run and disables the record it was only meant to rename. A three-state dropdown can say it. +export const LEAVE_UNCHANGED = 'unchanged'; + +export function booleanUpdateDropdown({ + displayName, + description, +}: { + displayName: string; + description: string; +}) { + return Property.StaticDropdown({ + displayName, + description, + required: false, + defaultValue: LEAVE_UNCHANGED, + options: { + options: [ + { label: 'Leave unchanged', value: LEAVE_UNCHANGED }, + { label: 'Yes', value: 'true' }, + { label: 'No', value: 'false' }, + ], + }, + }); +} + +// Only the dropdown's own string values change anything. A boolean `false` is what a step saved by +// the earlier checkbox version of these props left behind, where it meant "untouched" at least as +// often as it meant "disable" — it is ignored rather than replayed, so an existing flow stops +// disabling its target. Set the dropdown to "No" to disable deliberately. +export function readBooleanUpdate(value: unknown): boolean | undefined { + if (value === 'true' || value === true) return true; + if (value === 'false') return false; + return undefined; +} + // --- Additional Attributes / Relations (custom entity fields) ----------------- export const additionalAttributesProp = Property.Json({ @@ -730,6 +769,8 @@ export const additionalHeadersProp = Property.Object({ // --- Private helpers ---------------------------------------------------------- +type BooleanUpdateValue = typeof LEAVE_UNCHANGED | 'true' | 'false'; + function buildProductNameMap( included: { type: string; diff --git a/packages/pieces/community/orocommerce/test/action-guards.test.ts b/packages/pieces/community/orocommerce/test/action-guards.test.ts index cb407a08c65e..02a01acbcd4a 100644 --- a/packages/pieces/community/orocommerce/test/action-guards.test.ts +++ b/packages/pieces/community/orocommerce/test/action-guards.test.ts @@ -63,6 +63,43 @@ describe('create invoice rejects unusable line items before calling Oro', () => }); }); +// The builder seeds an unset checkbox with `false` and persists it, so before these props became +// three-state dropdowns "Update User, change the last name" also sent enabled: false and disabled +// the account. The empty-request guard is the observable: if the flag still reached `attributes`, +// there would be something to update and no error. +describe('an untouched boolean flag is not sent', () => { + it('Update User leaves Enabled alone', async () => { + await expect( + updateUserAction.run( + createMockActionContext({ + propsValue: { userId: '1', enabled: 'unchanged' }, + }) + ) + ).rejects.toThrow('Update User: nothing to update.'); + }); + + it('Update User ignores a `false` left behind by the checkbox version of the prop', async () => { + await expect( + updateUserAction.run( + createMockActionContext({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- the prop no longer accepts a boolean; an existing step input still holds one + propsValue: { userId: '1', enabled: false as any }, + }) + ) + ).rejects.toThrow('Update User: nothing to update.'); + }); + + it('Update Customer User leaves Enabled and Confirmed alone', async () => { + await expect( + updateCustomerUserAction.run( + createMockActionContext({ + propsValue: { customerUserId: '1', enabled: 'unchanged', confirmed: 'unchanged' }, + }) + ) + ).rejects.toThrow('Update Customer User: nothing to update.'); + }); +}); + describe('update actions refuse to send an empty request', () => { it('Update Customer', async () => { await expect( diff --git a/packages/pieces/community/orocommerce/test/boolean-update.test.ts b/packages/pieces/community/orocommerce/test/boolean-update.test.ts new file mode 100644 index 000000000000..4c5449f16025 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/boolean-update.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { booleanUpdateDropdown, readBooleanUpdate, LEAVE_UNCHANGED } from '../src/lib/common'; + +describe('readBooleanUpdate', () => { + it('sends a value only for the two explicit choices', () => { + expect(readBooleanUpdate('true')).toBe(true); + expect(readBooleanUpdate('false')).toBe(false); + }); + + it('sends nothing for the default choice or an absent value', () => { + expect(readBooleanUpdate(LEAVE_UNCHANGED)).toBeUndefined(); + expect(readBooleanUpdate(undefined)).toBeUndefined(); + expect(readBooleanUpdate('')).toBeUndefined(); + }); + + it('ignores the `false` a step saved by the checkbox version of the prop still holds', () => { + expect(readBooleanUpdate(false)).toBeUndefined(); + expect(readBooleanUpdate(true)).toBe(true); + }); +}); + +describe('booleanUpdateDropdown', () => { + it('offers three states and defaults to leaving the value alone', () => { + const prop = booleanUpdateDropdown({ + displayName: 'Enabled', + description: 'Enable or disable the user account.', + }); + + expect(prop.defaultValue).toBe(LEAVE_UNCHANGED); + expect(prop.required).toBe(false); + expect(prop.options.options.map((option) => option.value)).toEqual([ + LEAVE_UNCHANGED, + 'true', + 'false', + ]); + }); +}); From d4d6bf2ebdbdace92688f5dbcadd99d9e49e0d52 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 27 Aug 2026 15:37:49 +0200 Subject: [PATCH 43/50] fix(orocommerce): catch three requests Oro can only reject MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three were found by running the actions against a live Oro 7.1 instance; each one reached Oro and came back 400, or worse, succeeded wrongly. Product Unit on an invoice line item was optional in the piece and is not optional in Oro: leaving it empty answered 400 not blank constraint at /included/0/attributes/unitOfQuantity which names neither the row nor the field the user filled in. It is now required and validated through lineItemUtils.requiredString like Quantity, so the message says "Line Items row 1, "Product Unit" is required". No working flow changes behaviour — a flow that left it empty could not succeed before either. The invoice attachment was sent with a hardcoded mimeType of application/pdf whatever the file was, and Oro does not sniff the content: a PNG attached to Invoice PDF was accepted and stored as file 705, original_filename unknown.png, extension png, mime_type application/pdf. Nothing failed, so nothing said the invoice's default PDF is now a file that no PDF reader will open. Deriving the type from the extension was the other option; it is worse here, because the relationship this file lands on is invoiceDefaultPdfFile and Oro renders it as the invoice PDF, so a non-PDF is a mistake to report rather than a type to record. readPdfContent checks for the %PDF- signature and names the offending file. Re-verified live: the PNG is now refused with no invoice created, and a real PDF still lands - invoice 60, file 706, pr4-fix.pdf / pdf / application/pdf, all three agreeing. A null value in Additional Relations was passed through to Oro, which answers 400 request data constraint - The relationship should have 'data' property so the request could never have succeeded. It is rejected up front with the linkage forms spelled out. {"data": null} stays accepted and does clear a to-one relationship - confirmed against the rig, not assumed. Co-Authored-By: Claude Opus 5 (1M context) --- .../pieces/community/orocommerce/README.md | 6 +++ .../src/lib/actions/create-invoice.ts | 44 +++++++++++---- .../src/lib/common/jsonapi/body-utils.ts | 6 ++- .../orocommerce/test/action-guards.test.ts | 54 +++++++++++++++++-- .../orocommerce/test/body-utils.test.ts | 13 +++++ 5 files changed, 107 insertions(+), 16 deletions(-) diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index ba6bd59bbce3..9397d2338e22 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -352,6 +352,12 @@ client secret always come from the connection. actions keep their checkboxes, where an unchecked box and `false` mean the same thing. Note that a hand-written `propsValue` in `test/action-guards.test.ts` does not reproduce the builder's `false` — a case that stands in for a saved step has to pass it explicitly. + +- **The invoice attachment is sent as `application/pdf`, so `create-invoice` checks that it is one.** + Oro takes a file's type from the `mimeType` in the request and does not sniff the content: a PNG + attached to *Invoice PDF* was accepted and stored with extension `png` and mime type + `application/pdf`, which every consumer that trusts the type then serves as a broken PDF. + `readPdfContent` rejects anything whose first bytes are not `%PDF-` before the request is built. - Line-item input is validated through `lineItemUtils` (`src/lib/common/line-items.ts`), not bare `Number()`. `Number(undefined)` is `NaN` and `JSON.stringify` serialises `NaN` as `null`, so an unvalidated missing quantity used to reach Oro as `null` with no error. Route any new line-item diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index f1b52e63d9b2..fee1aa1133e6 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -157,8 +157,9 @@ export const createInvoiceAction = createAction({ }), unitOfQuantity: Property.ShortText({ displayName: 'Product Unit', - description: 'Unit of measurement (e.g. piece, kg, set).', - required: false, + description: + 'Unit of measurement (e.g. piece, kg, set). Oro rejects a line item without one.', + required: true, }), unitPrice: Property.Number({ displayName: 'Unit Price', @@ -223,6 +224,13 @@ export const createInvoiceAction = createAction({ displayName: LINE_ITEMS_DISPLAY_NAME, min: 0, }), + unitOfQuantity: lineItemUtils.requiredString({ + row, + index, + field: 'unitOfQuantity', + label: 'Product Unit', + displayName: LINE_ITEMS_DISPLAY_NAME, + }), unitPrice: lineItemUtils.requiredNumber({ row, index, @@ -239,13 +247,6 @@ export const createInvoiceAction = createAction({ displayName: LINE_ITEMS_DISPLAY_NAME, }), ...jsonApiBodyUtils.pickDefined({ - unitOfQuantity: lineItemUtils.optionalString({ - row, - index, - field: 'unitOfQuantity', - label: 'Product Unit', - displayName: LINE_ITEMS_DISPLAY_NAME, - }), note: lineItemUtils.optionalString({ row, index, @@ -277,10 +278,10 @@ export const createInvoiceAction = createAction({ type: 'files', id: 'invoiceDefaultPdfFile', attributes: { - mimeType: 'application/pdf', + mimeType: PDF_MIME_TYPE, originalFilename: p.invoicePdfFilename || p.invoicePdfContent.filename, - content: p.invoicePdfContent.base64, + content: readPdfContent(p.invoicePdfContent), }, } : undefined; @@ -348,3 +349,24 @@ export const createInvoiceAction = createAction({ }); const LINE_ITEMS_DISPLAY_NAME = 'Line Items'; + +const PDF_MIME_TYPE = 'application/pdf'; + +const PDF_SIGNATURE = '%PDF-'; + +// Oro takes the file's type from the mimeType we send, and this file becomes the invoice's default +// PDF, so PDF_MIME_TYPE is the only value that makes sense here. Which means the attachment really +// has to be a PDF: sending anything else stores it under a type it is not, and every consumer that +// trusts the type — the back-office PDF download included — then serves a broken document. Checking +// the signature keeps the hardcoded type honest and says which file was wrong while the step can +// still be fixed. +function readPdfContent(file: { filename: string; base64: string }): string { + // Eight base64 characters decode to the first six bytes — enough for the signature. + const header = Buffer.from(file.base64.slice(0, 8), 'base64').toString('latin1'); + if (!header.startsWith(PDF_SIGNATURE)) { + throw new Error( + `Invoice PDF: "${file.filename}" is not a PDF (it does not start with "${PDF_SIGNATURE}"). Oro stores this file as the invoice's default PDF, so attach a PDF or convert the file in an earlier step.` + ); + } + return file.base64; +} diff --git a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts index 0d7f4d864868..ac66b0a3b2cb 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts @@ -65,7 +65,11 @@ function parseAdditionalRelations( } const obj = parsed as Record; for (const [key, value] of Object.entries(obj)) { - if (value === null) continue; + if (value === null) { + throw new Error( + `Additional Relations: "${key}" is null. A relationship is a linkage object: {"data": {"type": "myentities", "id": "1"}} for one record, {"data": [...]} for several, {"data": null} for none.` + ); + } if (typeof value !== 'object') { throw new Error( `Additional Relations: "${key}" must be a JSON:API linkage object with a "data" key, e.g. {"data": {"type": "myentities", "id": "1"}}.` diff --git a/packages/pieces/community/orocommerce/test/action-guards.test.ts b/packages/pieces/community/orocommerce/test/action-guards.test.ts index 02a01acbcd4a..065c46f7639f 100644 --- a/packages/pieces/community/orocommerce/test/action-guards.test.ts +++ b/packages/pieces/community/orocommerce/test/action-guards.test.ts @@ -5,6 +5,12 @@ import { updateCustomerAction } from '../src/lib/actions/update-customer'; import { updateUserAction } from '../src/lib/actions/update-user'; import { updateCustomerUserAction } from '../src/lib/actions/update-customer-user'; +// Smallest valid PNG and the opening bytes of a PDF, base64-encoded. +const PNG_BASE64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg=='; + +const PDF_BASE64 = Buffer.from('%PDF-1.7\n1 0 obj\n', 'latin1').toString('base64'); + function invoiceProps(lineItems: Record[]) { return { invoiceDate: '2026-01-01', @@ -27,7 +33,7 @@ describe('create invoice rejects unusable line items before calling Oro', () => it('reports a missing quantity instead of sending null', async () => { await expect( runInvoice([ - { description: 'Widget', unitPrice: 10, rowTotal: 100 }, + { description: 'Widget', unitOfQuantity: 'piece', unitPrice: 10, rowTotal: 100 }, ]) ).rejects.toThrow('Line Items row 1, "Quantity" must be a number, got no value.'); }); @@ -35,21 +41,29 @@ describe('create invoice rejects unusable line items before calling Oro', () => it('reports a non-numeric quantity with the offending value', async () => { await expect( runInvoice([ - { description: 'Widget', quantity: 'ten', unitPrice: 10, rowTotal: 100 }, + { description: 'Widget', quantity: 'ten', unitOfQuantity: 'piece', unitPrice: 10, rowTotal: 100 }, ]) ).rejects.toThrow('Line Items row 1, "Quantity" must be a number, got "ten".'); }); it('reports a missing description', async () => { await expect( - runInvoice([{ quantity: 10, unitPrice: 10, rowTotal: 100 }]) + runInvoice([{ quantity: 10, unitOfQuantity: 'piece', unitPrice: 10, rowTotal: 100 }]) ).rejects.toThrow('Line Items row 1, "Description" is required, got no value.'); }); + // Oro answers a blank unitOfQuantity with a 400 "not blank" constraint on + // /included/0/attributes/unitOfQuantity, which names neither the row nor the field the user filled in. + it('reports a missing product unit rather than letting Oro reject the request', async () => { + await expect( + runInvoice([{ description: 'Widget', quantity: 10, unitPrice: 10, rowTotal: 100 }]) + ).rejects.toThrow('Line Items row 1, "Product Unit" is required, got no value.'); + }); + it('reports a total that does not match the row totals', async () => { await expect( runInvoice([ - { description: 'Widget', quantity: 1, unitPrice: 10, rowTotal: 10 }, + { description: 'Widget', quantity: 1, unitOfQuantity: 'piece', unitPrice: 10, rowTotal: 10 }, ]) ).rejects.toThrow( '"Total Amount" is 100, but the sum of "Row Total" across 1 row(s) is 10.' @@ -63,6 +77,38 @@ describe('create invoice rejects unusable line items before calling Oro', () => }); }); +describe('create invoice refuses to file a non-PDF as the invoice PDF', () => { + function runWithFile(file: { filename: string; base64: string }) { + return createInvoiceAction.run( + createMockActionContext({ + propsValue: { + ...invoiceProps([ + { description: 'Widget', quantity: 10, unitOfQuantity: 'piece', unitPrice: 10, rowTotal: 100 }, + ]), + invoicePdfContent: file, + }, + }) + ); + } + + it('names the file when the content is not a PDF', async () => { + await expect( + // A one-pixel PNG: Oro accepts it and stores it as application/pdf, because that is the + // mimeType this action sends for every attachment. + runWithFile({ filename: 'chart.png', base64: PNG_BASE64 }) + ).rejects.toThrow( + 'Invoice PDF: "chart.png" is not a PDF (it does not start with "%PDF-").' + ); + }); + + it('accepts content that carries the PDF signature', async () => { + // Gets as far as the API call, which the mock context has no connection for — so validation passed. + await expect( + runWithFile({ filename: 'invoice.pdf', base64: PDF_BASE64 }) + ).rejects.toThrow('OroCommerce API Error'); + }); +}); + // The builder seeds an unset checkbox with `false` and persists it, so before these props became // three-state dropdowns "Update User, change the last name" also sent enabled: false and disabled // the account. The empty-request guard is the observable: if the flag still reached `attributes`, diff --git a/packages/pieces/community/orocommerce/test/body-utils.test.ts b/packages/pieces/community/orocommerce/test/body-utils.test.ts index fdd2f6f08d55..d358feb2cae4 100644 --- a/packages/pieces/community/orocommerce/test/body-utils.test.ts +++ b/packages/pieces/community/orocommerce/test/body-utils.test.ts @@ -117,6 +117,19 @@ describe('jsonApiBodyUtils.parseAdditionalRelations', () => { /must be a JSON object/ ); }); + + // A bare null used to pass straight through to Oro, which answers + // 400 "The relationship should have 'data' property" — a request that could never succeed. + it('rejects a null value and shows the linkage forms', () => { + expect(() => + jsonApiBodyUtils.parseAdditionalRelations({ myRelation: null }) + ).toThrow('Additional Relations: "myRelation" is null.'); + }); + + it('accepts an explicit empty linkage', () => { + const value = { myRelation: { data: null }, myOtherRelation: { data: [] } }; + expect(jsonApiBodyUtils.parseAdditionalRelations(value)).toEqual(value); + }); }); describe('jsonApiBodyUtils.assertUpdateNotEmpty', () => { From a34c4e70b527af026beeb53b71a30b98fefcf788 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 27 Aug 2026 15:38:00 +0200 Subject: [PATCH 44/50] docs(orocommerce): record the two prerequisites the live run cost an hour on Neither is discoverable from the piece, and both look like something else. The OAuth application's organization scopes every record the connection can reach. On the rig, customers 1-4 belong to ACME, Inc and the client's user to Oro, and every one of them answers 403 No access to the entity - the same status a missing permission gives, so the first hour went into roles that were already correct. Customer 5 worked immediately. The webhook trigger cannot be published until the entity is marked webhook accessible in Oro. Until then the Topic dropdown is empty for that entity and publishing fails with valid webhook topic constraint, which reads as a bug in the trigger. Co-Authored-By: Claude Opus 5 (1M context) --- packages/pieces/community/orocommerce/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 9397d2338e22..d3747684e1ea 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -30,6 +30,13 @@ Activepieces verifies the connection with `GET regions/US-CA`. If the OAuth appl cannot read `regions`, the connection is reported invalid even when the credentials are correct — grant that permission or the check will keep failing. +**The OAuth application's organization scopes every record the connection can reach.** A customer, +order or user that belongs to another organization answers `403 No access to the entity` — the same +status a missing permission produces, so it reads as an authentication problem when it is not one. If +a record you can see in the back office is invisible to a step, check the organization on the OAuth +application's user before touching its roles. On a multi-organization instance you need one connection +per organization. + ## Actions | Action | What it does | @@ -52,6 +59,10 @@ to-many relationship is a **full replace** — see *Update actions replace to-ma dropdown lists only the topics your connection can read. Enabling the trigger registers the webhook in Oro; disabling it removes the registration. +An entity publishes no topics until it is opened up in Oro: **System → Entities → Entity Management → +the entity → Webhook accessible = Yes**. Until then the Topic dropdown offers nothing for it, and +publishing a flow whose trigger names one of its topics fails with `valid webhook topic constraint`. + ## Reporting issues Open an issue at with your OroCommerce version, the From aa7a15a15c88ea798420db8d49fbc271f141fc5e Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 27 Aug 2026 15:52:14 +0200 Subject: [PATCH 45/50] chore: restore the .nvmrc trailing newline The only change this branch makes to .nvmrc is stripping the newline at end of file - same version, v24.14.0, before and after. An editor artifact, and it sits in a root file upstream owns, so it would conflict on a future sync for no benefit. Co-Authored-By: Claude Opus 5 (1M context) --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index f9c0a7587323..a4a7a41bca7c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.14.0 \ No newline at end of file +v24.14.0 From be83d53fc20af3f73b7ef375f35b8ceb30c1addc Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Fri, 28 Aug 2026 10:14:38 +0200 Subject: [PATCH 46/50] revert(orocommerce): drop the non-piece changes from poc/orocommerce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit poc/orocommerce is the branch proposed upstream, so it carries the piece and nothing else. Reverted: the i18n:check task and the piece filter added to .github/workflows/ci.yml, the i18n:check and i18n:write tasks in turbo.json, the whole of .github/workflows/oro-ci.yml, and an accidental trailing newline in .nvmrc. bun.lock keeps the workspace entry. All 727 community pieces on main have one, and without it the next bun install re-adds it. The i18n gate moves inside the piece: test/i18n.test.ts runs tools/check-i18n.mjs, so the root test task covers it with no task of its own in turbo.json. i18n:write stays a package script and never needed one. tools/check-scope.mjs is what stops this recurring — it diffs origin/poc/orocommerce...HEAD and fails on any file outside the package. Nothing in .github/workflows/ now runs the piece's checks on the fork. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 2 +- .github/workflows/oro-ci.yml | 38 ------------ .nvmrc | 2 +- .../pieces/community/orocommerce/package.json | 3 +- .../community/orocommerce/test/i18n.test.ts | 17 ++++++ .../orocommerce/tools/check-scope.mjs | 58 +++++++++++++++++++ turbo.json | 10 ---- 7 files changed, 79 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/oro-ci.yml create mode 100644 packages/pieces/community/orocommerce/test/i18n.test.ts create mode 100644 packages/pieces/community/orocommerce/tools/check-scope.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e55dc6def64..15baa200de48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,7 @@ jobs: set -euo pipefail pids=() - npx turbo run test i18n:check --filter=@activepieces/engine --filter=@activepieces/shared --filter=@activepieces/sandbox --filter=@activepieces/ai-providers --filter=@activepieces/pieces-framework --filter=web --filter=@activepieces/piece-orocommerce & + npx turbo run test --filter=@activepieces/engine --filter=@activepieces/shared --filter=@activepieces/sandbox --filter=@activepieces/ai-providers --filter=@activepieces/pieces-framework --filter=web & pids+=($!) npx turbo run test-ce test-ee test-cloud check-migrations --filter=api & diff --git a/.github/workflows/oro-ci.yml b/.github/workflows/oro-ci.yml deleted file mode 100644 index 2b4b383981f2..000000000000 --- a/.github/workflows/oro-ci.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Oro CI - -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: oro-ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - orocommerce-piece: - name: orocommerce piece - runs-on: ubuntu-latest - if: github.repository != 'activepieces/activepieces' - steps: - - uses: actions/checkout@v5 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - uses: actions/cache@v4 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} - restore-keys: ${{ runner.os }}-bun- - - - run: bun install - - - name: Build, lint, test and check translations - run: npx turbo run build lint test i18n:check --filter=@activepieces/piece-orocommerce diff --git a/.nvmrc b/.nvmrc index a4a7a41bca7c..f9c0a7587323 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.14.0 +v24.14.0 \ No newline at end of file diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index e4090d34078b..f6503b4ea4b1 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -31,7 +31,8 @@ "bundle": "node ../../../../dist/packages/cli/src/index.js pieces bundle", "test": "vitest run", "i18n:check": "node tools/check-i18n.mjs", - "i18n:write": "node tools/check-i18n.mjs --write" + "i18n:write": "node tools/check-i18n.mjs --write", + "check-scope": "node tools/check-scope.mjs" }, "devDependencies": { "vitest": "3.2.6", diff --git a/packages/pieces/community/orocommerce/test/i18n.test.ts b/packages/pieces/community/orocommerce/test/i18n.test.ts new file mode 100644 index 000000000000..7d88519caa0d --- /dev/null +++ b/packages/pieces/community/orocommerce/test/i18n.test.ts @@ -0,0 +1,17 @@ +import { spawnSync } from 'node:child_process'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const PACKAGE_ROOT = join(__dirname, '..'); + +describe('i18n', () => { + it('keeps src/i18n in sync with the piece metadata', () => { + const { status, stdout, stderr } = spawnSync( + process.execPath, + ['tools/check-i18n.mjs'], + { cwd: PACKAGE_ROOT, encoding: 'utf8' } + ); + + expect(status, `${stdout}${stderr}`).toBe(0); + }); +}); diff --git a/packages/pieces/community/orocommerce/tools/check-scope.mjs b/packages/pieces/community/orocommerce/tools/check-scope.mjs new file mode 100644 index 000000000000..543324969b72 --- /dev/null +++ b/packages/pieces/community/orocommerce/tools/check-scope.mjs @@ -0,0 +1,58 @@ +import { execFileSync } from 'node:child_process'; + +const PIECE_PATH = 'packages/pieces/community/orocommerce/'; +const ALLOWED_OUTSIDE_PIECE = new Set(['bun.lock']); +const DEFAULT_BASE = 'origin/main'; + +function git(args) { + return execFileSync('git', args, { encoding: 'utf8' }).trim(); +} + +function readOption({ args, name, fallback }) { + const hit = args.find((arg) => arg.startsWith(`--${name}=`)); + return hit ? hit.slice(name.length + 3) : fallback; +} + +function run() { + const args = process.argv.slice(2); + const base = readOption({ args, name: 'base', fallback: DEFAULT_BASE }); + + try { + git(['rev-parse', '--verify', `${base}^{commit}`]); + } catch { + console.error( + `error: ${base} is not a known ref. Run "git fetch origin" first, or pass --base=.` + ); + process.exit(1); + } + + const changed = git(['diff', '--name-only', `${base}...HEAD`]) + .split('\n') + .filter(Boolean); + const offenders = changed.filter( + (file) => !file.startsWith(PIECE_PATH) && !ALLOWED_OUTSIDE_PIECE.has(file) + ); + + if (offenders.length > 0) { + console.error( + `error: ${offenders.length} file(s) outside ${PIECE_PATH} changed since ${base}:` + ); + for (const file of offenders) { + console.error(` - ${file}`); + } + console.error( + `\npoc/orocommerce is the branch proposed upstream, so it carries the piece and nothing else.` + + `\nIf these are upstream's own changes, ${base} is stale — run "git fetch origin" and retry.` + ); + process.exit(1); + } + + const allowed = changed.filter((file) => ALLOWED_OUTSIDE_PIECE.has(file)); + console.log( + `scope check passed: ${changed.length} file(s) changed since ${base}, ` + + `${changed.length - allowed.length} inside ${PIECE_PATH}` + + `${allowed.length > 0 ? ` and ${allowed.join(', ')}` : ''}.` + ); +} + +run(); diff --git a/turbo.json b/turbo.json index 642aad143780..44111d9b5775 100644 --- a/turbo.json +++ b/turbo.json @@ -113,16 +113,6 @@ "inputs": ["src/**", "test/**", "tsconfig*.json", "vitest.config.*"], "outputs": ["coverage/**"] }, - "i18n:check": { - "dependsOn": ["build"], - "inputs": ["src/**", "tools/**", "package.json"], - "outputs": [] - }, - "i18n:write": { - "dependsOn": ["build"], - "cache": false, - "outputs": [] - }, "test-ce": { "dependsOn": ["build"], "cache": false From 31c28bd4c73e29fd05d167def27337d91bf07642 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Fri, 28 Aug 2026 10:16:06 +0200 Subject: [PATCH 47/50] fix(orocommerce): apply the review comments from PR #4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refCustomerId and refCustomerUserId are integers in Oro, so they go back to Property.Number. Making them ShortText was wrong: the Oro field is typed, and the arbitrary-external-id reading of the description does not survive that. invoicePdfContent goes back to a base64 LongText. Property.File cannot be fed from a transformation: the engine's file processor takes a data: URI or a URL and nothing else, so a bare base64 string from an earlier step falls through to new URL(), throws, and is swallowed into null — the action would then file the invoice with no PDF and no error. It now accepts base64 with or without the data: prefix, keeps the %PDF- signature check, and falls back to invoice.pdf for the filename. Long prop descriptions break the builder layout. The four password descriptions, the update-user action description, the confirmed checkbox and the connection's custom-headers description are cut to one line each; what they explained is in the README. Co-Authored-By: Claude Opus 5 (1M context) --- .../pieces/community/orocommerce/README.md | 40 +++++++++------- .../community/orocommerce/src/i18n/de.json | 16 +++---- .../community/orocommerce/src/i18n/fr.json | 16 +++---- .../community/orocommerce/src/i18n/nl.json | 16 +++---- .../community/orocommerce/src/i18n/pl.json | 16 +++---- .../orocommerce/src/i18n/translation.json | 16 +++---- .../community/orocommerce/src/i18n/uk.json | 16 +++---- .../src/lib/actions/create-customer-user.ts | 4 +- .../src/lib/actions/create-invoice.ts | 48 +++++++++++-------- .../src/lib/actions/create-user.ts | 2 +- .../src/lib/actions/update-customer-user.ts | 2 +- .../src/lib/actions/update-user.ts | 4 +- .../orocommerce/src/lib/common/auth.ts | 6 +-- .../orocommerce/test/action-guards.test.ts | 42 +++++++++++++--- 14 files changed, 142 insertions(+), 102 deletions(-) diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index d3747684e1ea..3b7863635501 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -249,21 +249,26 @@ those containers unconditionally without emitting empty ones on the wire. ## Local development ```bash -npx turbo run test --filter=@activepieces/piece-orocommerce # vitest (builds first) -npx turbo run i18n:check --filter=@activepieces/piece-orocommerce # i18n gate (builds first) -npx turbo run lint --filter=@activepieces/piece-orocommerce -npx turbo run build --filter=@activepieces/piece-orocommerce # tsc -p tsconfig.lib.json, also the type-check -npm run lint-dev # repo-wide lint with auto-fix +npx turbo run test --filter=@activepieces/piece-orocommerce # vitest, i18n gate included (builds first) +npx turbo run lint --filter=@activepieces/piece-orocommerce +npx turbo run build --filter=@activepieces/piece-orocommerce # tsc -p tsconfig.lib.json, also the type-check +npm run check-scope # nothing outside this directory changed +npm run lint-dev # repo-wide lint with auto-fix ``` There is no `typecheck` script in this package, so the root `typecheck` task is a no-op here — the build is the type-check. -`.github/workflows/ci.yml` runs `test` and `i18n:check` for this piece by name, alongside the other -packages in its hardcoded filter list. `test/jsonapi-roundtrip.test.ts` guards the -serialize/deserialize contract above, `test/line-items.test.ts` guards line-item validation, -`test/body-utils.test.ts` guards the request-body helpers, and `test/action-guards.test.ts` guards -the checks that stop an action calling Oro with unusable input. +Nothing in `.github/workflows/` runs these — `poc/orocommerce` is the branch proposed upstream, so +it carries this directory and `bun.lock` and nothing else. `npm run check-scope` is what enforces +that: it diffs `origin/main...HEAD` and fails on any file outside this package other than +`bun.lock`. Pass `--base=origin/poc/orocommerce` to scope it to one PR, and fetch first — a stale +base ref reports upstream's own changes as offenders. + +`test/jsonapi-roundtrip.test.ts` guards the serialize/deserialize contract above, +`test/line-items.test.ts` guards line-item validation, `test/body-utils.test.ts` guards the +request-body helpers, `test/action-guards.test.ts` guards the checks that stop an action calling Oro +with unusable input, and `test/i18n.test.ts` runs the i18n gate below. ## The i18n gate @@ -271,17 +276,18 @@ the checks that stop an action calling Oro with unusable input. translations. Both are generated, not hand-maintained: ```bash -npm run cli pieces generate-translation-file orocommerce # canonical; writes translation.json only -npx turbo run i18n:write --filter=@activepieces/piece-orocommerce # also reconciles the locale files (builds first) +npm run cli pieces generate-translation-file orocommerce # canonical; writes translation.json only +npm run build && npm run i18n:write # also reconciles the locale files ``` The two are not interchangeable. The CLI rewrites `translation.json` and nothing else, and it writes no trailing newline; `i18n:write` rewrites all six files and does. Prefer `i18n:write` — it is the one that keeps the locale files in step with the source. -`npm run i18n:check` (`tools/check-i18n.mjs`) fails the build when they drift. It imports the -**built** piece from `dist/` and only checks that the file exists, never that it is current — run it -through turbo (`npx turbo run i18n:check`), which builds first. It walks the same 19 metadata paths as +`npm run i18n:check` (`tools/check-i18n.mjs`) fails when they drift, and `test/i18n.test.ts` runs it +as part of the suite so the root `test` task covers it without a task of its own in the root +`turbo.json`. It imports the **built** piece from `dist/` and only checks that the file exists, never +that it is current — run it through turbo (`npx turbo run test`), which builds first. It walks the same 19 metadata paths as `pieceTranslation.pathsToValuesToTranslate` in `packages/pieces/framework/src/lib/i18n.ts`, and truncates keys at 512 characters exactly as the official generator does. It fails on keys missing from or stale in `translation.json`, on any locale file whose key set differs from it, and on empty @@ -299,7 +305,9 @@ Four actions take a password: `create-user`, `update-user`, `create-customer-use `update-customer-user`. Their values are ordinary step inputs — rendered in clear text in the builder, persisted in the flow version, and stored in step inputs. Run-log input truncation (`AP_FLOW_RUN_LOG_INPUT_TRUNCATE_THRESHOLD_KB`, 2 KB) does not help; a password is far under the -threshold. The prop descriptions say so, which is the only mitigation available today. +threshold. The prop descriptions point at a secret store, which is the only mitigation available +today. `update-user` can change username, email, password and auth status in one call, so it can +lock an existing user out of their account. There is no `Property.SecretText` to switch to. `SecretTextProperty` exists, but only as a `PieceAuthProperty` reachable through `PieceAuth.SecretText`, and it is deliberately absent from the diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json index b6c88243248d..f61066c22961 100644 --- a/packages/pieces/community/orocommerce/src/i18n/de.json +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -10,7 +10,7 @@ "The admin panel URL prefix (default is \"admin\").": "Das URL-Präfix des Adminbereichs (Standard ist \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "Die OAuth-Client-ID aus Ihrer OroCommerce-OAuth-Anwendung.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Das OAuth-Client-Secret aus Ihrer OroCommerce-OAuth-Anwendung.", - "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.": "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.", "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthentifizieren Sie sich bei den OroCommerce-APIs mit OAuth 2.0 Client Credentials.\n\n**Schritte zum Abrufen der Zugangsdaten:**\n1. Melden Sie sich in Ihrem OroCommerce-Administrationsbereich an.\n2. Navigieren Sie zu **System** > **User Management** > **OAuth Applications**.\n3. Klicken Sie auf **Create OAuth Application** und konfigurieren Sie:\n - **Application Name**: Geben Sie einen aussagekräftigen Namen ein (z. B. \"Activepieces Integration\")\n - **Grants**: Wählen Sie **Client Credentials**\n - **Redirect URIs**: Für den Client-Credentials-Flow nicht erforderlich\n4. Speichern Sie die Anwendung und kopieren Sie die **Client ID** und das **Client Secret**.\n5. Notieren Sie Ihre **Server URL** (z. B. `https://your-store.com`) und das **Admin Prefix** (in der Regel `admin`).\n ", "Create Invoice": "Rechnung erstellen", @@ -31,7 +31,7 @@ "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", - "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Führt einen direkten authentifizierten Aufruf der OroCommerce JSON:API aus.", "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", @@ -52,7 +52,7 @@ "Shipping Method": "Versandmethode", "Seller Info": "Verkäuferinformationen", "External Payment URL": "Externe Zahlungs-URL", - "Invoice PDF": "Invoice PDF", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organisation", "Owner": "Owner", @@ -162,8 +162,8 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Beschriftung der Versandmethode (z. B. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "HTML-Zeichenkette für Verkäuferkontakt/-adresse.", "URL for the external payment page.": "URL für die externe Zahlungsseite.", - "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", - "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.": "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.", + "Filename for the attached PDF. Defaults to invoice.pdf.": "Filename for the attached PDF. Defaults to invoice.pdf.", "The organization this record belongs to.": "The organization this record belongs to.", "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", "The website this record is associated with.": "The website this record is associated with.", @@ -204,17 +204,17 @@ "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", - "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Password for the new account. Prefer a value from a secret store over a literal one.": "Password for the new account. Prefer a value from a secret store over a literal one.", "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", - "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Marks the account confirmed without sending a confirmation email. Defaults to true.": "Marks the account confirmed without sending a confirmation email. Defaults to true.", "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", "Updated email address of the customer user.": "Updated email address of the customer user.", - "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "New password for the account. Prefer a value from a secret store over a literal one.": "New password for the account. Prefer a value from a secret store over a literal one.", "Enable or disable the storefront account.": "Enable or disable the storefront account.", "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json index f372c26cff17..c43c26177ca1 100644 --- a/packages/pieces/community/orocommerce/src/i18n/fr.json +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -10,7 +10,7 @@ "The admin panel URL prefix (default is \"admin\").": "Le préfixe d'URL du panneau d'administration (valeur par défaut \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "L'ID client OAuth de votre application OAuth OroCommerce.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Le secret client OAuth de votre application OAuth OroCommerce.", - "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.": "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.", "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthentifiez-vous auprès des API OroCommerce en utilisant les identifiants client OAuth 2.0.\n\n**Étapes pour obtenir les identifiants :**\n1. Connectez-vous à votre panneau d'administration OroCommerce.\n2. Accédez à **System** > **User Management** > **OAuth Applications**.\n3. Cliquez sur **Create OAuth Application** et configurez :\n - **Application Name** : Saisissez un nom descriptif (par ex. \"Activepieces Integration\")\n - **Grants** : Sélectionnez **Client Credentials**\n - **Redirect URIs** : Non requis pour le flux Client Credentials\n4. Enregistrez l'application et copiez le **Client ID** et le **Client Secret**.\n5. Notez votre **Server URL** (par ex. `https://your-store.com`) et le **Admin Prefix** (généralement `admin`).\n ", "Create Invoice": "Créer une facture", @@ -31,7 +31,7 @@ "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", - "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Effectue un appel authentifié direct vers le JSON:API d'OroCommerce.", "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", @@ -52,7 +52,7 @@ "Shipping Method": "Méthode d'expédition", "Seller Info": "Informations vendeur", "External Payment URL": "URL de paiement externe", - "Invoice PDF": "Invoice PDF", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organisation", "Owner": "Owner", @@ -162,8 +162,8 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Libellé de la méthode d'expédition (par ex. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "Chaîne HTML du contact/adresse du vendeur.", "URL for the external payment page.": "URL de la page de paiement externe.", - "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", - "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.": "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.", + "Filename for the attached PDF. Defaults to invoice.pdf.": "Filename for the attached PDF. Defaults to invoice.pdf.", "The organization this record belongs to.": "The organization this record belongs to.", "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", "The website this record is associated with.": "The website this record is associated with.", @@ -204,17 +204,17 @@ "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", - "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Password for the new account. Prefer a value from a secret store over a literal one.": "Password for the new account. Prefer a value from a secret store over a literal one.", "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", - "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Marks the account confirmed without sending a confirmation email. Defaults to true.": "Marks the account confirmed without sending a confirmation email. Defaults to true.", "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", "Updated email address of the customer user.": "Updated email address of the customer user.", - "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "New password for the account. Prefer a value from a secret store over a literal one.": "New password for the account. Prefer a value from a secret store over a literal one.", "Enable or disable the storefront account.": "Enable or disable the storefront account.", "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json index 0fae6aa6194d..400157b97cc1 100644 --- a/packages/pieces/community/orocommerce/src/i18n/nl.json +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -10,7 +10,7 @@ "The admin panel URL prefix (default is \"admin\").": "Het URL-prefix van het beheerpaneel (standaard \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "De OAuth Client-ID uit uw OroCommerce OAuth-applicatie.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Het OAuth Clientgeheim uit uw OroCommerce OAuth-applicatie.", - "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.": "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.", "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthenticeer bij de OroCommerce-API's met OAuth 2.0 Client Credentials.\n\n**Stappen om referenties te verkrijgen:**\n1. Meld u aan bij uw OroCommerce-beheerpaneel.\n2. Ga naar **System** > **User Management** > **OAuth Applications**.\n3. Klik op **Create OAuth Application** en configureer:\n - **Application Name**: Voer een beschrijvende naam in (bijv. \"Activepieces Integration\")\n - **Grants**: Selecteer **Client Credentials**\n - **Redirect URIs**: Niet vereist voor de Client Credentials-flow\n4. Sla de applicatie op en kopieer de **Client ID** en **Client Secret**.\n5. Noteer uw **Server URL** (bijv. `https://your-store.com`) en **Admin Prefix** (meestal `admin`).\n ", "Create Invoice": "Factuur aanmaken", @@ -31,7 +31,7 @@ "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", - "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Voer een directe geauthenticeerde oproep uit naar de OroCommerce JSON:API.", "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", @@ -52,7 +52,7 @@ "Shipping Method": "Verzendmethode", "Seller Info": "Verkoperinfo", "External Payment URL": "Externe betalings-URL", - "Invoice PDF": "Invoice PDF", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organisatie", "Owner": "Owner", @@ -162,8 +162,8 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Label voor verzendmethode (bijv. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "HTML-tekenreeks voor verkoperscontact/adres.", "URL for the external payment page.": "URL voor de externe betalingspagina.", - "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", - "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.": "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.", + "Filename for the attached PDF. Defaults to invoice.pdf.": "Filename for the attached PDF. Defaults to invoice.pdf.", "The organization this record belongs to.": "The organization this record belongs to.", "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", "The website this record is associated with.": "The website this record is associated with.", @@ -204,17 +204,17 @@ "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", - "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Password for the new account. Prefer a value from a secret store over a literal one.": "Password for the new account. Prefer a value from a secret store over a literal one.", "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", - "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Marks the account confirmed without sending a confirmation email. Defaults to true.": "Marks the account confirmed without sending a confirmation email. Defaults to true.", "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", "Updated email address of the customer user.": "Updated email address of the customer user.", - "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "New password for the account. Prefer a value from a secret store over a literal one.": "New password for the account. Prefer a value from a secret store over a literal one.", "Enable or disable the storefront account.": "Enable or disable the storefront account.", "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json index 885a4501cc49..b08d43cbdf78 100644 --- a/packages/pieces/community/orocommerce/src/i18n/pl.json +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -10,7 +10,7 @@ "The admin panel URL prefix (default is \"admin\").": "Prefiks URL panelu administracyjnego (domyślnie \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "Identyfikator klienta OAuth z Twojej aplikacji OAuth w OroCommerce.", "The OAuth Client Secret from your OroCommerce OAuth application.": "Sekret klienta OAuth z Twojej aplikacji OAuth w OroCommerce.", - "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.": "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.", "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nUwierzytelnij się w API OroCommerce, używając danych klienta OAuth 2.0 (Client Credentials).\n\n**Kroki, aby uzyskać dane logowania:**\n1. Zaloguj się do panelu administracyjnego OroCommerce.\n2. Przejdź do **System** > **User Management** > **OAuth Applications**.\n3. Kliknij **Create OAuth Application** i skonfiguruj:\n - **Application Name**: Wpisz opisową nazwę (np. \"Activepieces Integration\")\n - **Grants**: Wybierz **Client Credentials**\n - **Redirect URIs**: Niewymagane dla przepływu Client Credentials\n4. Zapisz aplikację i skopiuj wartości **Client ID** oraz **Client Secret**.\n5. Zanotuj **Server URL** (np. `https://your-store.com`) oraz **Admin Prefix** (zwykle `admin`).\n ", "Create Invoice": "Utwórz fakturę", @@ -31,7 +31,7 @@ "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", - "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Wykonuje bezpośrednie, uwierzytelnione wywołanie do OroCommerce JSON:API.", "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", @@ -52,7 +52,7 @@ "Shipping Method": "Metoda wysyłki", "Seller Info": "Informacje o sprzedawcy", "External Payment URL": "Zewnętrzny URL płatności", - "Invoice PDF": "Invoice PDF", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organizacja", "Owner": "Owner", @@ -162,8 +162,8 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Etykieta metody wysyłki (np. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "Ciąg HTML kontaktu/adresu sprzedawcy.", "URL for the external payment page.": "Adres URL zewnętrznej strony płatności.", - "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", - "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.": "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.", + "Filename for the attached PDF. Defaults to invoice.pdf.": "Filename for the attached PDF. Defaults to invoice.pdf.", "The organization this record belongs to.": "The organization this record belongs to.", "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", "The website this record is associated with.": "The website this record is associated with.", @@ -204,17 +204,17 @@ "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", - "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Password for the new account. Prefer a value from a secret store over a literal one.": "Password for the new account. Prefer a value from a secret store over a literal one.", "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", - "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Marks the account confirmed without sending a confirmation email. Defaults to true.": "Marks the account confirmed without sending a confirmation email. Defaults to true.", "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", "Updated email address of the customer user.": "Updated email address of the customer user.", - "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "New password for the account. Prefer a value from a secret store over a literal one.": "New password for the account. Prefer a value from a secret store over a literal one.", "Enable or disable the storefront account.": "Enable or disable the storefront account.", "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json index f409130ee79d..b14d304ebc84 100644 --- a/packages/pieces/community/orocommerce/src/i18n/translation.json +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -10,7 +10,7 @@ "The admin panel URL prefix (default is \"admin\").": "The admin panel URL prefix (default is \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "The OAuth Client ID from your OroCommerce OAuth application.", "The OAuth Client Secret from your OroCommerce OAuth application.": "The OAuth Client Secret from your OroCommerce OAuth application.", - "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.": "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.", "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **Client ID** and **Client Secret**.\n5. Note your **Server URL** (e.g., `https://your-store.com`) and **Admin Prefix** (usually `admin`).\n ", "Create Invoice": "Create Invoice", @@ -31,7 +31,7 @@ "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", - "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Make a direct authenticated call to the OroCommerce JSON:API.", "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", @@ -52,7 +52,7 @@ "Shipping Method": "Shipping Method", "Seller Info": "Seller Info", "External Payment URL": "External Payment URL", - "Invoice PDF": "Invoice PDF", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Organization", "Owner": "Owner", @@ -162,8 +162,8 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "Seller contact / address HTML string.", "URL for the external payment page.": "URL for the external payment page.", - "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", - "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.": "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.", + "Filename for the attached PDF. Defaults to invoice.pdf.": "Filename for the attached PDF. Defaults to invoice.pdf.", "The organization this record belongs to.": "The organization this record belongs to.", "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", "The website this record is associated with.": "The website this record is associated with.", @@ -204,17 +204,17 @@ "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", - "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Password for the new account. Prefer a value from a secret store over a literal one.": "Password for the new account. Prefer a value from a secret store over a literal one.", "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", - "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Marks the account confirmed without sending a confirmation email. Defaults to true.": "Marks the account confirmed without sending a confirmation email. Defaults to true.", "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", "Updated email address of the customer user.": "Updated email address of the customer user.", - "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "New password for the account. Prefer a value from a secret store over a literal one.": "New password for the account. Prefer a value from a secret store over a literal one.", "Enable or disable the storefront account.": "Enable or disable the storefront account.", "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json index 35960a900274..58728843e342 100644 --- a/packages/pieces/community/orocommerce/src/i18n/uk.json +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -10,7 +10,7 @@ "The admin panel URL prefix (default is \"admin\").": "Префікс URL адмін-панелі (за замовчуванням \"admin\").", "The OAuth Client ID from your OroCommerce OAuth application.": "OAuth Client ID з вашого OAuth-застосунку OroCommerce.", "The OAuth Client Secret from your OroCommerce OAuth application.": "OAuth Client Secret з вашого OAuth-застосунку OroCommerce.", - "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}": "JSON object of HTTP headers sent with every action of this piece, including Custom API Call. A header set on the step itself wins over the same header set here. Authorization is always managed by this connection and cannot be overridden per step. Example: {\"Content-Type\": \"application/vnd.api+json\", \"X-Custom\": \"value\"}", + "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.": "JSON object of HTTP headers sent with every action. A header set on the step wins; Authorization is always managed by this connection.", "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.": "Setting this option allows to rewrite network-related connection options with values provied in ENV variables.", "\nAuthenticate to OroCommerce APIs using OAuth 2.0 Client Credentials.\n\n**Steps to obtain credentials:**\n1. Log in to your OroCommerce admin panel.\n2. Navigate to **System** > **User Management** > **OAuth Applications**.\n3. Click **Create OAuth Application** and configure:\n - **Application Name**: Enter a descriptive name (e.g., \"Activepieces Integration\")\n - **Grants**: Select **Client Credentials**\n - **Redirect URIs**: Not required for Client Credentials flow\n4. Save the application and copy the **": "\nАвтентифікуйтеся в API OroCommerce за допомогою OAuth 2.0 Client Credentials.\n\n**Кроки для отримання облікових даних:**\n1. Увійдіть в адмін-панель OroCommerce.\n2. Перейдіть до **System** > **User Management** > **OAuth Applications**.\n3. Натисніть **Create OAuth Application** та налаштуйте:\n - **Application Name**: Вкажіть описову назву (наприклад, \"Activepieces Integration\")\n - **Grants**: Оберіть **Client Credentials**\n - **Redirect URIs**: Не потрібно для потоку Client Credentials\n4. Збережіть застосунок і скопіюйте **Client ID** та **Client Secret**.\n5. Запам’ятайте ваш **Server URL** (наприклад, `https://your-store.com`) та **Admin Prefix** (зазвичай `admin`).\n ", "Create Invoice": "Створити рахунок-фактуру", @@ -31,7 +31,7 @@ "Creates a new customer user (storefront account) in OroCommerce.": "Creates a new customer user (storefront account) in OroCommerce.", "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.": "Updates an existing customer user (storefront account) in OroCommerce. Only provided fields are changed.", "Creates a new back-office user in OroCommerce.": "Creates a new back-office user in OroCommerce.", - "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.", + "Updates an existing back-office user in OroCommerce. Only provided fields are changed.": "Updates an existing back-office user in OroCommerce. Only provided fields are changed.", "Make a direct authenticated call to the OroCommerce JSON:API.": "Виконує прямий автентифікований виклик до OroCommerce JSON:API.", "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.": "Builds a JSON:API request body from a plain object, ready for the Request Body of the API Call action.", "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.": "Flattens a JSON:API response body into a plain object. Included relationships are inlined, and the result stays re-serializable.", @@ -52,7 +52,7 @@ "Shipping Method": "Спосіб доставки", "Seller Info": "Інформація про продавця", "External Payment URL": "Зовнішній URL оплати", - "Invoice PDF": "Invoice PDF", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", "Invoice PDF Filename": "Invoice PDF Filename", "Organization": "Організація", "Owner": "Owner", @@ -162,8 +162,8 @@ "Shipping method label (e.g. \"International Shipping (Tracking #: 123)\").": "Мітка способу доставки (наприклад, \"International Shipping (Tracking #: 123)\").", "Seller contact / address HTML string.": "HTML-рядок контактів / адреси продавця.", "URL for the external payment page.": "URL зовнішньої сторінки оплати.", - "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.": "PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.", - "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.": "Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.", + "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.": "Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.", + "Filename for the attached PDF. Defaults to invoice.pdf.": "Filename for the attached PDF. Defaults to invoice.pdf.", "The organization this record belongs to.": "The organization this record belongs to.", "The back-office user who owns this record. Search by name, username or email.": "The back-office user who owns this record. Search by name, username or email.", "The website this record is associated with.": "The website this record is associated with.", @@ -204,17 +204,17 @@ "Customer addresses to create along with the customer.": "Customer addresses to create along with the customer.", "The numeric ID of the customer to update.": "The numeric ID of the customer to update.", "Email address of the customer user. Used as the login.": "Email address of the customer user. Used as the login.", - "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "Password for the new account. Prefer a value from a secret store over a literal one.": "Password for the new account. Prefer a value from a secret store over a literal one.", "Honorific (e.g. Mr., Ms., Dr.).": "Honorific (e.g. Mr., Ms., Dr.).", "Suffix (e.g. PhD, Jr.).": "Suffix (e.g. PhD, Jr.).", "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.": "When disabled the user cannot log into the storefront. Defaults to true, so the account is active as soon as it is created.", - "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.": "Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.", + "Marks the account confirmed without sending a confirmation email. Defaults to true.": "Marks the account confirmed without sending a confirmation email. Defaults to true.", "Birth date in YYYY-MM-DD format.": "Birth date in YYYY-MM-DD format.", "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.": "The complete set of customer user roles. Saving replaces the existing roles, so include every role the customer user should keep.", "Customer user addresses to create along with the user.": "Customer user addresses to create along with the user.", "The numeric ID of the customer user to update.": "The numeric ID of the customer user to update.", "Updated email address of the customer user.": "Updated email address of the customer user.", - "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.": "New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.", + "New password for the account. Prefer a value from a secret store over a literal one.": "New password for the account. Prefer a value from a secret store over a literal one.", "Enable or disable the storefront account.": "Enable or disable the storefront account.", "Whether the user has completed email confirmation.": "Whether the user has completed email confirmation.", "Login name for the user. Must be unique.": "Login name for the user. Must be unique.", diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts index b8a745146385..3636910261cb 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -34,7 +34,7 @@ export const createCustomerUserAction = createAction({ password: Property.ShortText({ displayName: 'Password', description: - 'Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', + 'Password for the new account. Prefer a value from a secret store over a literal one.', required: true, }), @@ -76,7 +76,7 @@ export const createCustomerUserAction = createAction({ confirmed: Property.Checkbox({ displayName: 'Confirmed', description: - 'Whether the user has completed email confirmation. Defaults to true, which marks the account confirmed without sending a confirmation email. Uncheck it to require the user to confirm their address.', + 'Marks the account confirmed without sending a confirmation email. Defaults to true.', required: false, defaultValue: true, }), diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts index fee1aa1133e6..28f4e2981186 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -42,13 +42,13 @@ export const createInvoiceAction = createAction({ }), customer: customerDropdown, customerUser: customerUserDropdown(false), - refCustomerId: Property.ShortText({ + refCustomerId: Property.Number({ displayName: 'External Customer ID', description: 'An optional ID reference to a customer. Can be used for storing an arbitrary external ID.', required: false, }), - refCustomerUserId: Property.ShortText({ + refCustomerUserId: Property.Number({ displayName: 'External Customer User ID', description: 'An optional ID reference to a customer user. Can be used for storing an arbitrary external ID.', @@ -108,16 +108,15 @@ export const createInvoiceAction = createAction({ description: 'URL for the external payment page.', required: false, }), - invoicePdfContent: Property.File({ - displayName: 'Invoice PDF', + invoicePdfContent: Property.LongText({ + displayName: 'Invoice PDF (Base64)', description: - 'PDF file to attach to the invoice as the default PDF. Accepts a file from a previous step or a URL.', + 'Base64-encoded PDF, with or without a data: prefix. Attached to the invoice as its default PDF.', required: false, }), invoicePdfFilename: Property.ShortText({ displayName: 'Invoice PDF Filename', - description: - 'Filename for the attached PDF (e.g. invoice.pdf). Defaults to the uploaded file name.', + description: 'Filename for the attached PDF. Defaults to invoice.pdf.', required: false, }), @@ -273,15 +272,18 @@ export const createInvoiceAction = createAction({ id: li.id, })); + const pdfFilename = p.invoicePdfFilename || DEFAULT_PDF_FILENAME; const pdfFile = p.invoicePdfContent ? { type: 'files', id: 'invoiceDefaultPdfFile', attributes: { mimeType: PDF_MIME_TYPE, - originalFilename: - p.invoicePdfFilename || p.invoicePdfContent.filename, - content: readPdfContent(p.invoicePdfContent), + originalFilename: pdfFilename, + content: readPdfContent({ + content: p.invoicePdfContent, + filename: pdfFilename, + }), }, } : undefined; @@ -354,19 +356,23 @@ const PDF_MIME_TYPE = 'application/pdf'; const PDF_SIGNATURE = '%PDF-'; -// Oro takes the file's type from the mimeType we send, and this file becomes the invoice's default -// PDF, so PDF_MIME_TYPE is the only value that makes sense here. Which means the attachment really -// has to be a PDF: sending anything else stores it under a type it is not, and every consumer that -// trusts the type — the back-office PDF download included — then serves a broken document. Checking -// the signature keeps the hardcoded type honest and says which file was wrong while the step can -// still be fixed. -function readPdfContent(file: { filename: string; base64: string }): string { - // Eight base64 characters decode to the first six bytes — enough for the signature. - const header = Buffer.from(file.base64.slice(0, 8), 'base64').toString('latin1'); +const DEFAULT_PDF_FILENAME = 'invoice.pdf'; + +const DATA_URI_PREFIX = /^data:[^;,]*;base64,/; + +function readPdfContent({ + content, + filename, +}: { + content: string; + filename: string; +}): string { + const base64 = content.replace(DATA_URI_PREFIX, '').trim(); + const header = Buffer.from(base64.slice(0, 8), 'base64').toString('latin1'); if (!header.startsWith(PDF_SIGNATURE)) { throw new Error( - `Invoice PDF: "${file.filename}" is not a PDF (it does not start with "${PDF_SIGNATURE}"). Oro stores this file as the invoice's default PDF, so attach a PDF or convert the file in an earlier step.` + `Invoice PDF: "${filename}" is not a PDF (it does not start with "${PDF_SIGNATURE}"). Oro stores this file as the invoice's default PDF, so pass a base64 PDF or convert the file in an earlier step.` ); } - return file.base64; + return base64; } diff --git a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts index 292b245c0651..26adec470f47 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts @@ -37,7 +37,7 @@ export const createUserAction = createAction({ password: Property.ShortText({ displayName: 'Password', description: - 'Password for the new account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', + 'Password for the new account. Prefer a value from a secret store over a literal one.', required: true, }), firstName: Property.ShortText({ diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts index d9f28123944c..bd35989a19da 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -40,7 +40,7 @@ export const updateCustomerUserAction = createAction({ password: Property.ShortText({ displayName: 'Password', description: - 'New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', + 'New password for the account. Prefer a value from a secret store over a literal one.', required: false, }), diff --git a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts index 397b22ed130e..e09487bac6c7 100644 --- a/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -24,7 +24,7 @@ export const updateUserAction = createAction({ name: 'update_user', displayName: 'Update User', description: - 'Updates an existing back-office user in OroCommerce. Only provided fields are changed. Username, email, password and auth status can all be changed in one call, so this action can lock an existing user out of their account.', + 'Updates an existing back-office user in OroCommerce. Only provided fields are changed.', props: { // -- Target record --------------------------------------------------------- userId: Property.ShortText({ @@ -47,7 +47,7 @@ export const updateUserAction = createAction({ password: Property.ShortText({ displayName: 'Password', description: - 'New password for the account. Must comply with the system security policy. Stored in clear text in this step and visible to anyone who can open the flow, so prefer a value carried in from a secret store over a literal one.', + 'New password for the account. Prefer a value from a secret store over a literal one.', required: false, }), firstName: Property.ShortText({ diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 971163776ede..9b75c018a98c 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -45,10 +45,8 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. headers: Property.LongText({ displayName: 'Default HTTP Headers', description: - 'JSON object of HTTP headers sent with every action of this piece, including Custom API Call. ' + - 'A header set on the step itself wins over the same header set here. ' + - 'Authorization is always managed by this connection and cannot be overridden per step. ' + - 'Example: {"Content-Type": "application/vnd.api+json", "X-Custom": "value"}', + 'JSON object of HTTP headers sent with every action. A header set on the step wins; ' + + 'Authorization is always managed by this connection.', required: false }), isInternalInfrastructure: Property.Checkbox({ diff --git a/packages/pieces/community/orocommerce/test/action-guards.test.ts b/packages/pieces/community/orocommerce/test/action-guards.test.ts index 065c46f7639f..98fbae3afbb2 100644 --- a/packages/pieces/community/orocommerce/test/action-guards.test.ts +++ b/packages/pieces/community/orocommerce/test/action-guards.test.ts @@ -78,14 +78,21 @@ describe('create invoice rejects unusable line items before calling Oro', () => }); describe('create invoice refuses to file a non-PDF as the invoice PDF', () => { - function runWithFile(file: { filename: string; base64: string }) { + function runWithPdf({ + filename, + content, + }: { + filename: string; + content: string; + }) { return createInvoiceAction.run( createMockActionContext({ propsValue: { ...invoiceProps([ { description: 'Widget', quantity: 10, unitOfQuantity: 'piece', unitPrice: 10, rowTotal: 100 }, ]), - invoicePdfContent: file, + invoicePdfContent: content, + invoicePdfFilename: filename, }, }) ); @@ -93,18 +100,39 @@ describe('create invoice refuses to file a non-PDF as the invoice PDF', () => { it('names the file when the content is not a PDF', async () => { await expect( - // A one-pixel PNG: Oro accepts it and stores it as application/pdf, because that is the - // mimeType this action sends for every attachment. - runWithFile({ filename: 'chart.png', base64: PNG_BASE64 }) + runWithPdf({ filename: 'chart.png', content: PNG_BASE64 }) ).rejects.toThrow( 'Invoice PDF: "chart.png" is not a PDF (it does not start with "%PDF-").' ); }); + it('falls back to invoice.pdf when no filename is given', async () => { + await expect( + createInvoiceAction.run( + createMockActionContext({ + propsValue: { + ...invoiceProps([ + { description: 'Widget', quantity: 10, unitOfQuantity: 'piece', unitPrice: 10, rowTotal: 100 }, + ]), + invoicePdfContent: PNG_BASE64, + }, + }) + ) + ).rejects.toThrow('Invoice PDF: "invoice.pdf" is not a PDF'); + }); + it('accepts content that carries the PDF signature', async () => { - // Gets as far as the API call, which the mock context has no connection for — so validation passed. await expect( - runWithFile({ filename: 'invoice.pdf', base64: PDF_BASE64 }) + runWithPdf({ filename: 'invoice.pdf', content: PDF_BASE64 }) + ).rejects.toThrow('OroCommerce API Error'); + }); + + it('accepts a data: prefixed payload', async () => { + await expect( + runWithPdf({ + filename: 'invoice.pdf', + content: `data:application/pdf;base64,${PDF_BASE64}`, + }) ).rejects.toThrow('OroCommerce API Error'); }); }); From ba1657ceb030e0b320b2142ada27bad60ba06491 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Fri, 28 Aug 2026 10:17:46 +0200 Subject: [PATCH 48/50] fix(orocommerce): keep the signing change inside the piece Drop the breaking-changes.mdx entry: poc/orocommerce is the branch that becomes the upstream PR and must stay a piece-only diff. The OroCommerce 6.1 floor and the "untick before republishing on older instances" note now live in the piece README next to the checkbox they describe. Cut the Sign webhook deliveries description to one sentence under the 200-character prop limit and reseed its i18n key in every locale file. Co-Authored-By: Claude Fable 5 --- docs/install/reference/breaking-changes.mdx | 5 ----- packages/pieces/community/orocommerce/README.md | 4 ++++ packages/pieces/community/orocommerce/src/i18n/de.json | 2 +- packages/pieces/community/orocommerce/src/i18n/fr.json | 2 +- packages/pieces/community/orocommerce/src/i18n/nl.json | 2 +- packages/pieces/community/orocommerce/src/i18n/pl.json | 2 +- .../pieces/community/orocommerce/src/i18n/translation.json | 2 +- packages/pieces/community/orocommerce/src/i18n/uk.json | 2 +- .../orocommerce/src/lib/triggers/webhook-topic-trigger.ts | 4 +--- 9 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/install/reference/breaking-changes.mdx b/docs/install/reference/breaking-changes.mdx index 9a2e339856bd..2279bd757a52 100644 --- a/docs/install/reference/breaking-changes.mdx +++ b/docs/install/reference/breaking-changes.mdx @@ -88,10 +88,6 @@ Rolling back costs at most the codes issued after the deploy: the older build ca Sign-up now refuses addresses from throwaway providers such as `mailinator.com` and `guerrillamail.com`, on both the emailed-code flow and the password form. Federated sign-in (Google, SAML, JWT), managed authentication and SCIM provisioning are unaffected, since those addresses come from an identity provider you already trust. -#### The OroCommerce piece signs webhook deliveries by default - -The Oro Webhook Event trigger now registers its webhook with a signing secret and discards deliveries whose `Webhook-Signature` header does not match the delivered body. The secret attribute requires OroCommerce 6.1 or newer — on older versions, enabling, re-enabling or republishing a flow with this trigger fails at webhook registration until the trigger's **Sign webhook deliveries** checkbox is turned off. Flows that are already enabled keep running unverified until they are next re-enabled. - #### Plans and credits are now managed by our billing service Two license-key endpoints are removed: `GET /v1/license-keys/:licenseKey` and `POST /v1/license-keys/verify`. Applying a key is now `POST /v1/platform-billing/activate`. @@ -109,7 +105,6 @@ In the AI piece, picking the Activepieces-provided AI now lists only the named t ### Do you need to take action? - Only if your members sign up with addresses from a disposable email provider. Set `AP_ALLOW_DISPOSABLE_EMAILS=true` to keep accepting them. -- Only if a flow uses the Oro Webhook Event trigger against an OroCommerce instance older than 6.1. Untick **Sign webhook deliveries** on the trigger before re-enabling or republishing that flow; existing enabled flows keep working until then. - Only if you run the Enterprise edition without a license key. Enter your key so your contracted limits apply instead of the free plan's. - Only if you call `GET /v1/license-keys/:licenseKey` or `POST /v1/license-keys/verify`. Both are removed — use `POST /v1/platform-billing/activate` instead. - Only if you read `SHOW_BILLING_PAGE`, `CAN_BUY_ACTIVE_FLOWS`, `CAN_BUY_AI_CREDITS` or `SHOW_BILLING_LIMITS_ON_SIDEBAR` from `GET /v1/flags`. They no longer exist. diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index 53895f4e7390..dbebd55ed322 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -72,6 +72,10 @@ anyone who learns the webhook URL can start the flow with a payload of their cho The secret cannot be read back or changed after registration. To rotate it, disable and re-enable the trigger, which deletes the old webhook and registers a new one. +Flows enabled before signing existed keep running unverified until they are next re-enabled or +republished. On an instance older than 6.1, turn the checkbox off before doing either, or the +registration fails and the flow stays disabled. + ## Reporting issues Open an issue at with your OroCommerce version, the diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json index da63dade985a..397aed67eff3 100644 --- a/packages/pieces/community/orocommerce/src/i18n/de.json +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." } diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json index 696d34a0f521..7f8b0a4259d3 100644 --- a/packages/pieces/community/orocommerce/src/i18n/fr.json +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." } diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json index 4eb2b21a9324..8304edcd3f56 100644 --- a/packages/pieces/community/orocommerce/src/i18n/nl.json +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." } diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json index 235feaaee3ce..ef7858e70d88 100644 --- a/packages/pieces/community/orocommerce/src/i18n/pl.json +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." } diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json index b4265595f753..a5227db09053 100644 --- a/packages/pieces/community/orocommerce/src/i18n/translation.json +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." } diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json index c543b39ea6dc..f4fd419cb0a3 100644 --- a/packages/pieces/community/orocommerce/src/i18n/uk.json +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." } diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index d05d9d77d734..b6965d402212 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -48,9 +48,7 @@ export const oroWebhookTopicTrigger = createTrigger({ signDeliveries: Property.Checkbox({ displayName: 'Sign webhook deliveries', description: - 'Register the webhook with a shared secret and discard deliveries whose "Webhook-Signature" header does not match the body. ' + - 'Requires OroCommerce 6.1 or newer: older versions reject the secret and the registration fails. ' + - 'Turn this off only for those versions, and be aware that anyone who learns the webhook URL can then start this flow.', + 'Register the webhook with a shared secret and discard deliveries whose "Webhook-Signature" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.', required: false, defaultValue: true, }), From 82a2758b473cee649b21ef3756fc0cc32586c789 Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Mon, 31 Aug 2026 08:33:05 +0200 Subject: [PATCH 49/50] fix(orocommerce): apply the review comments from PR #5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop every OroCommerce version mention from the signing feature. Webhooks and their secret shipped as one unit, so a build that has webhooks but rejects the secret does not exist — the "needs 6.1, turn off for older versions" framing described nothing. The checkbox stays, re-justified as the escape hatch for a proxy between Oro and Activepieces that re-encodes the body: the signature covers the exact bytes delivered, so such a proxy makes every delivery fail verification. The prop description is one sentence; the off-switch guidance lives in the README. Comment the stale-registration cleanup in onEnable with why it exists and what it prevents, as requested on review. Rewrite the README webhook section down from ~40 lines to the facts a maintainer acts on: what Oro signs, when verification runs, the two compensating deletes, and that a rejected delivery is silent — no run, Oro still gets its 200 — so a quiet signed trigger means checking the worker logs for "webhook delivery discarded". The engine-version archaeology and call-chain traces stay in the commit history. The description is an i18n key, so all six locale files are reseeded; the dropped translations were English seeds, nothing hand-translated is lost. Co-Authored-By: Claude Fable 5 --- .../pieces/community/orocommerce/README.md | 60 +++++++------------ .../community/orocommerce/src/i18n/de.json | 2 +- .../community/orocommerce/src/i18n/fr.json | 2 +- .../community/orocommerce/src/i18n/nl.json | 2 +- .../community/orocommerce/src/i18n/pl.json | 2 +- .../orocommerce/src/i18n/translation.json | 2 +- .../community/orocommerce/src/i18n/uk.json | 2 +- .../src/lib/triggers/webhook-topic-trigger.ts | 5 +- 8 files changed, 30 insertions(+), 47 deletions(-) diff --git a/packages/pieces/community/orocommerce/README.md b/packages/pieces/community/orocommerce/README.md index dbebd55ed322..3d267a4a062b 100644 --- a/packages/pieces/community/orocommerce/README.md +++ b/packages/pieces/community/orocommerce/README.md @@ -65,16 +65,16 @@ publishing a flow whose trigger names one of its topics fails with `valid webhoo **Sign webhook deliveries** is on by default. Enabling the trigger then generates a secret, hands it to Oro at registration, and every later delivery must carry a matching `Webhook-Signature` header or -it is discarded without starting a run. This needs **OroCommerce 6.1 or newer** — older versions -reject the secret and the registration fails, so turn the checkbox off for them. With signing off, -anyone who learns the webhook URL can start the flow with a payload of their choosing. +it is discarded without starting a run. Turn it off only when something between Oro and Activepieces +rewrites the request body — the signature covers the exact bytes delivered, so a proxy that re-encodes +the body makes every delivery fail verification. With signing off, anyone who learns the webhook URL +can start the flow with a payload of their choosing. The secret cannot be read back or changed after registration. To rotate it, disable and re-enable the trigger, which deletes the old webhook and registers a new one. Flows enabled before signing existed keep running unverified until they are next re-enabled or -republished. On an instance older than 6.1, turn the checkbox off before doing either, or the -registration fails and the flow stays disabled. +republished. ## Reporting issues @@ -262,41 +262,21 @@ those containers unconditionally without emitting empty ones on the wire. ## Webhook deliveries are verified against the raw body Oro signs the exact bytes it sends: `hash_hmac('sha256', rawBody, secret)`, hex, in the -`Webhook-Signature` header. `run` therefore verifies `context.payload.rawBody`, never a -re-serialized `context.payload.body` — JSON round-tripping reorders keys and changes whitespace, and -the digest would never match. - -Verification keys off *the trigger having stored a secret*, not off the header being present. Oro -sends no signature at all when a webhook has no secret configured, so trusting header presence would -let an attacker skip verification by omitting the header. A store entry written before this feature -existed has no `secret`, and that absence means "keep running unverified" — no migration, no version -field. - -`onEnable` creates the Oro webhook before it can store the secret, so a failing `store.put` would -otherwise leave a live webhook whose secret is unrecoverable and whose every delivery is discarded -forever. The `put` is wrapped: on failure the webhook is deleted (errors from that delete are -swallowed) and the original error is rethrown. - -`onEnable` also deletes a leftover registration found in the store before creating a replacement. -Republishing a flow calls `triggerSourceService.enable` without ever calling `disable` -(`flow.service.ts` → `flow-service-side-effects.ts` → `trigger-source-service.ts`), so `onEnable` -really does run without a matching `onDisable`; without that cleanup every republish would leak an -Oro webhook whose secret is no longer stored anywhere. The store is keyed by `flowId`, not by flow -version (`StoreScope.FLOW` in `packages/server/engine/src/lib/piece-context/store.ts`), so the -leftover entry is still visible at that point. - -A rejected delivery is logged with `console.warn` and returns `[]` — no run is created, the caller -still gets its 200, and no secret material reaches the log. The log line reads `context.flows` and -`context.step` through optional chaining: `step` only exists on the trigger run context since -Activepieces 0.71.0 (engine commit `d64d7bf8f3`), and a plain property access would turn "discard -the forged delivery" into a thrown `TypeError` on older engines — on the security path, of all -places. The success path returns -`[context.payload.body]`; returning the whole payload would change the output schema of every -existing flow. - -The HMAC comparison mirrors `verifyHmacAuth` in -`packages/pieces/core/webhook/src/lib/triggers/catch-hook.ts` — explicit length check, then -`timingSafeEqual`. It is copied rather than imported: pieces may not depend on each other. +`Webhook-Signature` header. Verification therefore covers `context.payload.rawBody`, never a +re-serialized `context.payload.body` — JSON round-tripping reorders keys and the digest would never +match. + +Verification runs only when this trigger has a secret stored. Oro sends no signature header when a +webhook has no secret, so header presence is never trusted. A store entry written before signing +existed has no secret, and that absence means "keep running unverified". + +`onEnable` deletes the webhook it just created when storing the secret fails — a live webhook whose +secret is unrecoverable would have every delivery discarded — and drops a leftover registration +before creating a replacement, because republishing a flow runs `onEnable` without `onDisable`. + +A rejected delivery returns `[]` with a `console.warn`: no run is created and Oro still gets its +200, so a wrong secret looks like silence. If a signed trigger goes quiet, check the worker logs for +"webhook delivery discarded". ## Local development diff --git a/packages/pieces/community/orocommerce/src/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json index 397aed67eff3..bd1f8eeb81c0 100644 --- a/packages/pieces/community/orocommerce/src/i18n/de.json +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body." } diff --git a/packages/pieces/community/orocommerce/src/i18n/fr.json b/packages/pieces/community/orocommerce/src/i18n/fr.json index 7f8b0a4259d3..efe400fd8bbf 100644 --- a/packages/pieces/community/orocommerce/src/i18n/fr.json +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body." } diff --git a/packages/pieces/community/orocommerce/src/i18n/nl.json b/packages/pieces/community/orocommerce/src/i18n/nl.json index 8304edcd3f56..e221320df57b 100644 --- a/packages/pieces/community/orocommerce/src/i18n/nl.json +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body." } diff --git a/packages/pieces/community/orocommerce/src/i18n/pl.json b/packages/pieces/community/orocommerce/src/i18n/pl.json index ef7858e70d88..10e2020b24b7 100644 --- a/packages/pieces/community/orocommerce/src/i18n/pl.json +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body." } diff --git a/packages/pieces/community/orocommerce/src/i18n/translation.json b/packages/pieces/community/orocommerce/src/i18n/translation.json index a5227db09053..ec635b292441 100644 --- a/packages/pieces/community/orocommerce/src/i18n/translation.json +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body." } diff --git a/packages/pieces/community/orocommerce/src/i18n/uk.json b/packages/pieces/community/orocommerce/src/i18n/uk.json index f4fd419cb0a3..d6cad2c9eb73 100644 --- a/packages/pieces/community/orocommerce/src/i18n/uk.json +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -258,5 +258,5 @@ "Topic": "Topic", "Sign webhook deliveries": "Sign webhook deliveries", "Only topics accessible by your connection are shown": "Only topics accessible by your connection are shown", - "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions." + "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body.": "Register the webhook with a shared secret and discard deliveries whose \"Webhook-Signature\" header does not match the body." } diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index b6965d402212..116b4b1bf947 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -48,7 +48,7 @@ export const oroWebhookTopicTrigger = createTrigger({ signDeliveries: Property.Checkbox({ displayName: 'Sign webhook deliveries', description: - 'Register the webhook with a shared secret and discard deliveries whose "Webhook-Signature" header does not match the body. Needs OroCommerce 6.1 or newer; turn off for older versions.', + 'Register the webhook with a shared secret and discard deliveries whose "Webhook-Signature" header does not match the body.', required: false, defaultValue: true, }), @@ -57,6 +57,9 @@ export const oroWebhookTopicTrigger = createTrigger({ sampleData: {}, async onEnable(context) { + // Republishing a flow runs onEnable without a matching onDisable, so the + // previous registration would stay live in Oro holding a secret nothing + // stores anymore. Drop it before creating the replacement. const staleInfo = await context.store.get('webhookInfo'); if (staleInfo !== null && staleInfo !== undefined) { await discardWebhook({ auth: context.auth, webhookId: staleInfo.webhookId }); From c671bff4982e10fafe174d369b126e4d53f32b3d Mon Sep 17 00:00:00 2001 From: Mikhail Yahorau Date: Thu, 3 Sep 2026 11:27:54 +0200 Subject: [PATCH 50/50] fix(orocommerce): bump the piece to 0.3.0 0.2.0 is already installed out there, and POST /v1/pieces rejects a second upload of the same name+version with 409 piece_metadata_already_exists, so this build could not be installed over it at all. Verified on a CE 0.88.1 rig. Minor rather than patch because the webhook trigger gains a signDeliveries prop. Also fixes the why-comment on the onEnable stale-registration cleanup. It said republishing runs onEnable without a matching onDisable; on 0.88.1 LOCK_AND_PUBLISH calls onDisable first, and republishing an enabled flow leaves exactly one webhook row in Oro with or without the cleanup. Keeping the cleanup as a guard for enable-without-disable paths, and saying so. Co-Authored-By: Claude Opus 5 (1M context) --- bun.lock | 2 +- packages/pieces/community/orocommerce/package.json | 2 +- .../orocommerce/src/lib/triggers/webhook-topic-trigger.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 342613e51eb6..47e0aaaab960 100644 --- a/bun.lock +++ b/bun.lock @@ -6589,7 +6589,7 @@ }, "packages/pieces/community/orocommerce": { "name": "@activepieces/piece-orocommerce", - "version": "0.2.0", + "version": "0.3.0", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json index e4090d34078b..2ebbba8137db 100644 --- a/packages/pieces/community/orocommerce/package.json +++ b/packages/pieces/community/orocommerce/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-orocommerce", - "version": "0.2.0", + "version": "0.3.0", "description": "Activepieces piece for the OroCommerce back-office JSON:API: create and update customers, customer users, back-office users, orders and invoices, and trigger flows from OroCommerce webhooks.", "keywords": [ "activepieces", diff --git a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts index 116b4b1bf947..ecb617d610ba 100644 --- a/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -57,8 +57,8 @@ export const oroWebhookTopicTrigger = createTrigger({ sampleData: {}, async onEnable(context) { - // Republishing a flow runs onEnable without a matching onDisable, so the - // previous registration would stay live in Oro holding a secret nothing + // Defensive: on any path where onEnable runs without a matching onDisable, + // the previous registration would stay live in Oro holding a secret nothing // stores anymore. Drop it before creating the replacement. const staleInfo = await context.store.get('webhookInfo'); if (staleInfo !== null && staleInfo !== undefined) {