diff --git a/bun.lock b/bun.lock index e1b7ded2424a..47e0aaaab960 100644 --- a/bun.lock +++ b/bun.lock @@ -6587,6 +6587,20 @@ "tslib": "^2.3.0", }, }, + "packages/pieces/community/orocommerce": { + "name": "@activepieces/piece-orocommerce", + "version": "0.3.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", @@ -12022,6 +12036,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/.eslintrc.json b/packages/pieces/community/orocommerce/.eslintrc.json new file mode 100644 index 000000000000..89ae4dffbfff --- /dev/null +++ b/packages/pieces/community/orocommerce/.eslintrc.json @@ -0,0 +1,61 @@ +{ + "extends": [ + "../../../../.eslintrc.base.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "lodash", + "lodash/*", + "@activepieces/core-*", + "@activepieces/server*", + "@activepieces/engine", + "@activepieces/shared" + ] + } + ] + } + }, + { + "files": [ + "*.js", + "*.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 new file mode 100644 index 000000000000..400c339e4d51 --- /dev/null +++ b/packages/pieces/community/orocommerce/README.md @@ -0,0 +1,415 @@ +# OroCommerce piece + +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. + +## 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. + +**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 | +| --- | --- | +| **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. + +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`. + +**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. 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. + +## 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 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 + +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. + +## 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. 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 + +```bash +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. + +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 + +`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; 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 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 +values. Values identical to the English source are a warning; `--strict-untranslated` promotes them +to errors. + +`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 + +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 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 +`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 + +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. +- 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. +- **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. 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. + +- **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 + field through the helper. diff --git a/packages/pieces/community/orocommerce/package.json b/packages/pieces/community/orocommerce/package.json new file mode 100644 index 000000000000..9cf79956d9c9 --- /dev/null +++ b/packages/pieces/community/orocommerce/package.json @@ -0,0 +1,41 @@ +{ + "name": "@activepieces/piece-orocommerce", + "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", + "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": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/core-piece-types": "workspace:*", + "@activepieces/core-utils": "workspace:*" + }, + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "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", + "i18n:write": "node tools/check-i18n.mjs --write", + "check-scope": "node tools/check-scope.mjs" + }, + "devDependencies": { + "vitest": "3.2.6", + "tslib": "2.6.2" + } +} 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/i18n/de.json b/packages/pieces/community/orocommerce/src/i18n/de.json new file mode 100644 index 000000000000..a868beeaed6b --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/de.json @@ -0,0 +1,262 @@ +{ + "B2B digital commerce solution": "B2B-Digitalkommerzlösung", + "Server URL": "Server-URL", + "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. 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", + "Create Order": "Bestellung erstellen", + "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.": "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.", + "Invoice Date": "Rechnungsdatum", + "Currency": "Währung", + "Customer Name": "Kundenname", + "Customer": "Kunde", + "Customer User": "Kundenbenutzer", + "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", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", + "Invoice PDF Filename": "Invoice PDF Filename", + "Organization": "Organisation", + "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", + "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", + "Warehouse": "Lager", + "Parent Order": "Übergeordnete Bestellung", + "Status": "Status", + "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: Region / State": "Rechnung: Region / Bundesland", + "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: 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", + "Headers": "Header", + "Query Parameters": "Abfrageparameter", + "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", + "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.", + "Select a Customer User.": "Wählen Sie einen Kundenbenutzer aus.", + "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.", + "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.", + "Invoice internal status (e.g. Draft, Open).": "Interner Rechnungsstatus (z. B. Draft, Open).", + "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.", + "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).", + "Search payment terms.": "Zahlungsbedingungen suchen.", + "Search warehouses.": "Lager suchen.", + "Search orders to use as the parent order.": "Bestellungen suchen, die als übergeordnete Bestellung verwendet werden sollen.", + "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).", + "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.", + "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.", + "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\").", + "Order line items.": "Bestellpositionen.", + "Search products.": "Produkte suchen.", + "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. 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.", + "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. 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.", + "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.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "PUT": "PUT", + "DELETE": "DELETE", + "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", + "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.": "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 new file mode 100644 index 000000000000..5db56583a20a --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/fr.json @@ -0,0 +1,262 @@ +{ + "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", + "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. 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", + "Create Order": "Créer une commande", + "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.": "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.", + "Invoice Date": "Date de facture", + "Currency": "Devise", + "Customer Name": "Nom du client", + "Customer": "Client", + "Customer User": "Utilisateur client", + "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", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", + "Invoice PDF Filename": "Invoice PDF Filename", + "Organization": "Organisation", + "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", + "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", + "Warehouse": "Entrepôt", + "Parent Order": "Commande parente", + "Status": "Statut", + "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: Region / State": "Facturation : région / État", + "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: 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", + "Headers": "En-têtes", + "Query Parameters": "Paramètres de 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", + "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.", + "Select a Customer User.": "Sélectionnez un 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.", + "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.", + "Invoice internal status (e.g. Draft, Open).": "Statut interne de la facture (par ex. Draft, Open).", + "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.", + "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).", + "Search payment terms.": "Rechercher des conditions de paiement.", + "Search warehouses.": "Rechercher des entrepôts.", + "Search orders to use as the parent order.": "Rechercher des commandes à utiliser comme commande parente.", + "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é).", + "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.", + "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.", + "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\").", + "Order line items.": "Lignes de commande.", + "Search products.": "Rechercher des produits.", + "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. 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.", + "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. 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.", + "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.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "PUT": "PUT", + "DELETE": "DELETE", + "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", + "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.": "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 new file mode 100644 index 000000000000..acae5d3497ab --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/nl.json @@ -0,0 +1,262 @@ +{ + "B2B digital commerce solution": "B2B digitale commerce-oplossing", + "Server URL": "Server-URL", + "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. 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", + "Create Order": "Order aanmaken", + "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.": "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.", + "Invoice Date": "Factuurdatum", + "Currency": "Valuta", + "Customer Name": "Naam klant", + "Customer": "Klant", + "Customer User": "Klantgebruiker", + "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", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", + "Invoice PDF Filename": "Invoice PDF Filename", + "Organization": "Organisatie", + "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", + "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", + "Warehouse": "Magazijn", + "Parent Order": "Bovenliggende order", + "Status": "Status", + "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: Region / State": "Facturatie: regio / staat", + "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: 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", + "Headers": "Headers", + "Query Parameters": "Queryparameters", + "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", + "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.", + "Select a Customer User.": "Selecteer een klantgebruiker.", + "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.", + "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.", + "Invoice internal status (e.g. Draft, Open).": "Interne factuurstatus (bijv. Draft, Open).", + "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.", + "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).", + "Search payment terms.": "Zoek betalingstermijnen.", + "Search warehouses.": "Zoek magazijnen.", + "Search orders to use as the parent order.": "Zoek orders om als bovenliggende order te gebruiken.", + "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).", + "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.", + "Region or state. Select a country first. Start typing to filter.": "Regio of staat. Selecteer eerst een land. Begin te typen om te filteren.", + "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\").", + "Order line items.": "Orderregelitems.", + "Search products.": "Zoek producten.", + "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. 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.", + "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. 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.", + "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.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "PUT": "PUT", + "DELETE": "DELETE", + "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", + "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.": "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 new file mode 100644 index 000000000000..3a9cf22608f7 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/pl.json @@ -0,0 +1,262 @@ +{ + "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", + "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. 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ę", + "Create Order": "Utwórz zamówienie", + "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.": "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.", + "Invoice Date": "Data faktury", + "Currency": "Waluta", + "Customer Name": "Nazwa klienta", + "Customer": "Klient", + "Customer User": "Użytkownik klienta", + "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", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", + "Invoice PDF Filename": "Invoice PDF Filename", + "Organization": "Organizacja", + "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", + "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", + "Warehouse": "Magazyn", + "Parent Order": "Zamówienie nadrzędne", + "Status": "Status", + "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: Region / State": "Fakturowanie: region / województwo", + "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: 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", + "Headers": "Nagłówki", + "Query Parameters": "Parametry zapytania", + "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", + "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.", + "Select a Customer User.": "Wybierz użytkownika 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.", + "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.", + "Invoice internal status (e.g. Draft, Open).": "Wewnętrzny status faktury (np. Draft, Open).", + "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.", + "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).", + "Search payment terms.": "Wyszukaj terminy płatności.", + "Search warehouses.": "Wyszukaj magazyny.", + "Search orders to use as the parent order.": "Wyszukaj zamówienia do użycia jako zamówienie nadrzędne.", + "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).", + "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ę.", + "Region or state. Select a country first. Start typing to filter.": "Region lub województwo. Najpierw wybierz kraj. Zacznij pisać, aby filtrować.", + "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\").", + "Order line items.": "Pozycje zamówienia.", + "Search products.": "Wyszukaj produkty.", + "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. 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.", + "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. 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.", + "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.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "PUT": "PUT", + "DELETE": "DELETE", + "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", + "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.": "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 new file mode 100644 index 000000000000..d4617f9b732d --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/translation.json @@ -0,0 +1,262 @@ +{ + "B2B digital commerce solution": "B2B digital commerce solution", + "Server URL": "Server URL", + "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. 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", + "Create Order": "Create Order", + "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.": "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.", + "Invoice Date": "Invoice Date", + "Currency": "Currency", + "Customer Name": "Customer Name", + "Customer": "Customer", + "Customer User": "Customer User", + "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", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", + "Invoice PDF Filename": "Invoice PDF Filename", + "Organization": "Organization", + "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", + "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", + "Warehouse": "Warehouse", + "Parent Order": "Parent Order", + "Status": "Status", + "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: Region / State": "Billing: Region / State", + "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: 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", + "Headers": "Headers", + "Query Parameters": "Query Parameters", + "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", + "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.", + "Select a Customer User.": "Select a Customer User.", + "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.", + "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.", + "Invoice internal status (e.g. Draft, Open).": "Invoice internal status (e.g. Draft, Open).", + "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.", + "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).", + "Search payment terms.": "Search payment terms.", + "Search warehouses.": "Search warehouses.", + "Search orders to use as the parent order.": "Search orders to use as the parent order.", + "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).", + "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.", + "Region or state. Select a country first. Start typing to filter.": "Region or state. Select a country first. Start typing to filter.", + "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\").", + "Order line items.": "Order line items.", + "Search products.": "Search products.", + "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. 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.", + "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. 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.", + "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.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "PUT": "PUT", + "DELETE": "DELETE", + "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", + "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.": "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 new file mode 100644 index 000000000000..698d87fe9058 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/i18n/uk.json @@ -0,0 +1,262 @@ +{ + "B2B digital commerce solution": "B2B цифрове рішення для електронної комерції", + "Server URL": "URL сервера", + "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. 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": "Створити рахунок-фактуру", + "Create Order": "Створити замовлення", + "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.": "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.", + "Invoice Date": "Дата рахунку", + "Currency": "Валюта", + "Customer Name": "Назва клієнта", + "Customer": "Клієнт", + "Customer User": "Користувач клієнта", + "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 оплати", + "Invoice PDF (Base64)": "Invoice PDF (Base64)", + "Invoice PDF Filename": "Invoice PDF Filename", + "Organization": "Організація", + "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": "Примітки клієнта", + "Ship Until Date": "Відправити до дати", + "Overridden Shipping Cost": "Перевизначена вартість доставки", + "Estimated Shipping Cost": "Орієнтовна вартість доставки", + "Shipping Method Type": "Тип способу доставки", + "Disable Promotions": "Вимкнути промоакції", + "Payment Term": "Умови оплати", + "Warehouse": "Склад", + "Parent Order": "Батьківське замовлення", + "Status": "Статус", + "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: Region / State": "Платіжна адреса: регіон / штат", + "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: 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": "Метод", + "Headers": "Заголовки", + "Query Parameters": "Параметри запиту", + "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": "Атрибути", + "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.": "Оберіть клієнта.", + "Select a 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 зовнішньої сторінки оплати.", + "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.", + "Invoice internal status (e.g. Draft, Open).": "Внутрішній статус рахунку (наприклад, Draft, Open).", + "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), наданий покупцем.", + "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).", + "Search payment terms.": "Пошук умов оплати.", + "Search warehouses.": "Пошук складів.", + "Search orders to use as the parent order.": "Пошук замовлень для використання як батьківського замовлення.", + "Order status managed by an external system (only relevant when \"Enable External Status Management\" is on).": "Статус замовлення, яким керує зовнішня система (актуально лише коли ввімкнено \"Enable External Status Management\").", + "Address label (e.g. \"Main Office\").": "Мітка адреси (наприклад, \"Main Office\").", + "ISO-3166 country. Start typing to filter the list.": "Країна за ISO-3166. Почніть вводити, щоб відфільтрувати список.", + "Region or state. Select a country first. Start typing to filter.": "Регіон або штат. Спочатку оберіть країну. Почніть вводити, щоб відфільтрувати.", + "Free-text region for countries without predefined regions.": "Регіон у довільному форматі для країн без попередньо визначених регіонів.", + "Address label (e.g. \"Warehouse East\").": "Мітка адреси (наприклад, \"Warehouse East\").", + "Order line items.": "Позиції замовлення.", + "Search products.": "Пошук товарів.", + "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. 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.", + "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. 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.", + "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.", + "Leave unchanged": "Leave unchanged", + "Yes": "Yes", + "No": "No", + "GET": "GET", + "POST": "POST", + "PATCH": "PATCH", + "PUT": "PUT", + "DELETE": "DELETE", + "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", + "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.": "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/index.ts b/packages/pieces/community/orocommerce/src/index.ts new file mode 100644 index 000000000000..0ba3f610a2bb --- /dev/null +++ b/packages/pieces/community/orocommerce/src/index.ts @@ -0,0 +1,41 @@ +import { createPiece } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/pieces-framework'; +import { oroAuth } from './lib/common'; +import { oroWebhookTopicTrigger } from './lib/triggers/webhook-topic-trigger'; +import { + createInvoiceAction, + createOrderAction, + createCustomerAction, + updateCustomerAction, + createCustomerUserAction, + updateCustomerUserAction, + createUserAction, + updateUserAction, + customApiCallAction, + serializeJsonApiAction, + unserializeJsonApiAction, +} from './lib/actions'; + +export const orocommerce = createPiece({ + displayName: 'OroCommerce', + auth: oroAuth, + 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.'], + actions: [ + createInvoiceAction, + createOrderAction, + createCustomerAction, + updateCustomerAction, + createCustomerUserAction, + updateCustomerUserAction, + createUserAction, + updateUserAction, + customApiCallAction, + serializeJsonApiAction, + unserializeJsonApiAction, + ], + triggers: [oroWebhookTopicTrigger], +}); 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..bde3319ad9b7 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/api-call.ts @@ -0,0 +1,31 @@ +import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { + getAccessToken, + getConnectionHeaders, + getInternalInfrastructureHeaders, + getOroAdminApiBaseUrl, + oroAuth, + toHeaderRecord, +} 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 ? getOroAdminApiBaseUrl({ auth }) : '', + authMapping: async (auth, propsValue: Record) => ({ + ...getConnectionHeaders({ auth }), + ...getInternalInfrastructureHeaders({ auth }), + ...toHeaderRecord({ value: propsValue['headers'] }), + Authorization: `Bearer ${await getAccessToken({ auth })}`, + }), + props: { + headers: { + defaultValue: { + 'Accept': 'application/vnd.api+json', + 'X-Include': 'noHateoas;totalCount', + }, + }, + }, +}); 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..3636910261cb --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer-user.ts @@ -0,0 +1,191 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerRequiredDropdown, + customerUserRolesMultiDropdown, + organizationDropdown, + userDropdown, + websiteDropdown, + baseAddressArrayItemProps, + addressTypeProps, + buildIncludedAddress, + toAddressRow, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, + toHeaderRecord, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +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. Prefer a value from a secret store over a literal one.', + 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, so the account is active as soon as it is created.', + required: false, + defaultValue: true, + }), + confirmed: Property.Checkbox({ + displayName: 'Confirmed', + description: + 'Marks the account confirmed without sending a confirmation email. 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: customerUserRolesMultiDropdown, + 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, + additionalHeaders: additionalHeadersProp, + }, + + async run(context) { + const p = context.propsValue; + + const addresses = (p.addresses ?? []).flatMap((row, index) => { + const lid = `cu_addr_${index + 1}`; + 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); + + 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, + body, + 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 new file mode 100644 index 000000000000..14bc4736230f --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-customer.ts @@ -0,0 +1,149 @@ +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, + toAddressRow, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, + toHeaderRecord, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +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, + additionalHeaders: additionalHeadersProp, + }, + + async run(context) { + const p = context.propsValue; + + const addresses = (p.addresses ?? []).flatMap((row, index) => { + const lid = `addr_${index + 1}`; + 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); + + 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, + body, + 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 new file mode 100644 index 000000000000..28f4e2981186 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-invoice.ts @@ -0,0 +1,378 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerDropdown, + customerUserDropdown, + invoiceInternalStatusDropdown, + organizationDropdown, + userDropdown, + websiteDropdown, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, + toHeaderRecord, + lineItemUtils, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +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, + customerUser: customerUserDropdown(false), + 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.Number({ + 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, + }), + invoicePdfContent: Property.LongText({ + displayName: 'Invoice PDF (Base64)', + description: + '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. Defaults to invoice.pdf.', + required: false, + }), + + organization: organizationDropdown, + owner: userDropdown, + website: websiteDropdown, + internalStatus: invoiceInternalStatusDropdown, + + // -- Line Items ------------------------------------------------------------ + 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 () => { + 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, + }), + 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). Oro rejects a line item without one.', + 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, + }), + }, + }), + }; + }, + }), + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, + }, + + async run(context) { + const p = context.propsValue; + + const rows = lineItemUtils.readRows({ + value: p.lineItems, + arrayKey: 'lineItems', + displayName: LINE_ITEMS_DISPLAY_NAME, + }); + + const lineItemResources = rows.map((row, index) => ({ + type: 'invoicelineitems', + id: `li_${index + 1}`, + attributes: { + position: index + 1, + 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, + }), + unitOfQuantity: lineItemUtils.requiredString({ + row, + index, + field: 'unitOfQuantity', + label: 'Product Unit', + displayName: LINE_ITEMS_DISPLAY_NAME, + }), + 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: 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, + })); + + const pdfFilename = p.invoicePdfFilename || DEFAULT_PDF_FILENAME; + const pdfFile = p.invoicePdfContent + ? { + type: 'files', + id: 'invoiceDefaultPdfFile', + attributes: { + mimeType: PDF_MIME_TYPE, + originalFilename: pdfFilename, + content: readPdfContent({ + content: p.invoicePdfContent, + filename: pdfFilename, + }), + }, + } + : 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({ + refCustomerId: p.refCustomerId, + refCustomerUserId: p.refCustomerUserId, + 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, + }; + + 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 response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: '/invoices', + auth: context.auth, + body: { + data: { + type: 'invoices', + attributes, + relationships + }, + included, + }, + headers: toHeaderRecord({ value: p.additionalHeaders }), + }); + + return response.body; + }, +}); + +const LINE_ITEMS_DISPLAY_NAME = 'Line Items'; + +const PDF_MIME_TYPE = 'application/pdf'; + +const PDF_SIGNATURE = '%PDF-'; + +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: "${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 base64; +} 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..83d9ec64f7f7 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-order.ts @@ -0,0 +1,653 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + attrLabel, + loadDropdownOptions, + customerRequiredDropdown, + customerUserDropdown, + organizationDropdown, + userDropdown, + websiteDropdown, + orderInternalStatusDropdown, + orderStatusDropdown, + orderDropdown, + paymentTermDropdown, + warehouseDropdown, + buildCountryDropdown, + buildRegionDropdown, + productDropdown, + buildIncludedAddress, + type AddressRow, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, + toHeaderRecord, + lineItemUtils, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +export const createOrderAction = createAction({ + auth: oroAuth, + name: 'create_order', + displayName: 'Create Order', + description: 'Creates a new order record in OroCommerce.', + props: { + customer: customerRequiredDropdown, + customerUser: customerUserDropdown(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, + }), + 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 ------------------------------------------------ + organization: organizationDropdown, + owner: userDropdown, + website: websiteDropdown, + internalStatus: orderInternalStatusDropdown, + paymentTerm: paymentTermDropdown, + warehouse: warehouseDropdown, + parent: orderDropdown, + status: orderStatusDropdown, + + // -- 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({ + 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.', + 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({ + 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.', + required: false, + }), + + lineItems: Property.DynamicProperties({ + auth: oroAuth, + displayName: 'Line Items', + description: 'Order line items.', + required: true, + refreshers: [], + props: async ({ auth }) => { + 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({ + 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', + description: + 'Unique human-readable product identifier. Required.', + required: true, + }), + productUnit: Property.StaticDropdown({ + displayName: 'Product Unit', + description: 'Unit of measurement (e.g. piece, kg, set).', + required: true, + options: unitState, + }), + 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. Required.', + required: true, + }), + currency: Property.ShortText({ + displayName: 'Currency', + description: + 'ISO-4217 code. Defaults to order-level currency if empty.', + required: false, + }), + comment: Property.LongText({ + displayName: 'Comment', + description: 'Comments to the line item.', + required: false, + }), + shipBy: Property.ShortText({ + displayName: 'Ship By Date', + 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: warehouseState, + }), + }, + }), + }; + }, + }), + + // -- 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, + additionalHeaders: additionalHeadersProp, + }, + + async run(context) { + const p = context.propsValue; + + const billingAddressResource = buildAddressResource({ + localId: 'billing_address', + fields: { + 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, + }, + }); + + 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, + }, + }); + + // -- Line items ---------------------------------------------------------- + // DynamicProperties wraps the array in an object keyed by "lineItems" + const rows = lineItemUtils.readRows({ + value: p.lineItems, + arrayKey: 'lineItems', + displayName: LINE_ITEMS_DISPLAY_NAME, + }); + + 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); + + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes( + p.additionalAttributes + ); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations( + p.additionalRelations + ); + + // -- Order attributes ---------------------------------------------------- + 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', + auth: context.auth, + body: { + data: { + type: 'orders', + attributes, + relationships, + }, + included, + }, + headers: toHeaderRecord({ value: p.additionalHeaders }), + }); + + return response.body; + }, +}); + +function buildAddressResource({ + localId, + fields, +}: { + localId: string; + fields: AddressRow; +}): Record | null { + return buildIncludedAddress({ + lid: localId, + type: 'orderaddresses', + addr: fields, + extraAttributes: { fromExternalSource: false }, + }); +} + +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({ + row, + index, + orderCurrency, +}: { + row: Record; + index: number; + orderCurrency: string | null | undefined; +}): { resource: Record; ref: { type: string; id: string } } { + const lid = `li_${index + 1}`; + + const attributes: Record = { + fromExternalSource: true, + 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: 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', + }), + }), + }; + + const relationships: Record = jsonApiBodyUtils.buildRels({ + 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: 'warehouse', label: 'Warehouse' }), + ], + }); + + return { + resource: { type: 'orderlineitems', id: lid, attributes, relationships }, + ref: { type: 'orderlineitems', id: lid }, + }; +} + +const LINE_ITEMS_DISPLAY_NAME = 'Line Items'; + +type OrderFieldParams = { + row: Record; + index: number; + field: string; + label: string; +}; 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..26adec470f47 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/create-user.ts @@ -0,0 +1,162 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + businessUnitRequiredDropdown, + organizationDropdown, + userAuthStatusDropdown, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, + toHeaderRecord, + userRolesMultiDropdown, + userGroupsMultiDropdown, + organizationsMultiDropdown, + businessUnitsMultiDropdown, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +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. Prefer a value from a secret store over a literal one.', + 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: businessUnitsMultiDropdown, + userRoles: userRolesMultiDropdown, + organizations: organizationsMultiDropdown, + groups: userGroupsMultiDropdown, + 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, + body: { + data: { + type: 'users', + attributes, + relationships, + }, + }, + headers: toHeaderRecord({ value: p.additionalHeaders }), + }); + + 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..14f2eac00090 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/index.ts @@ -0,0 +1,11 @@ +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 { 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/serialize-jsonapi.ts b/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts new file mode 100644 index 000000000000..123070c9fa8a --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/serialize-jsonapi.ts @@ -0,0 +1,125 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +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: + '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: 'e.g. orders, invoices, products. Taken from _type when left empty.', + required: false, + }), + resourceId: Property.ShortText({ + displayName: 'Resource ID', + description: 'Leave empty to create a record, or when the input carries an id.', + required: false, + }), + attributes: Property.Json({ + displayName: 'Attributes', + description: + '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: + 'Wins over anything detected in Attributes. Example: {"customer":{"type":"customers","id":"42"}}', + required: false, + defaultValue: {}, + }), + included: Property.Json({ + displayName: 'Included', + description: + 'Extra resources to embed. Forwarded automatically when Attributes already has "included".', + required: false, + defaultValue: [], + }), + }, + + async run(context) { + const { resourceType, resourceId, attributes, relationships, included } = context.propsValue; + + const input: FlatResource = attributes ?? {}; + + const docIncluded = toResourceArray(input['included']); + const explicitIncluded = toResourceArray(included); + const mergedIncluded = explicitIncluded.length > 0 ? explicitIncluded : docIncluded; + + const flat = toFlatResource(input); + + const resolvedType = + (resourceType && resourceType.trim() !== '' ? resourceType.trim() : undefined) ?? + (typeof flat['_type'] === 'string' && flat['_type'].trim() !== '' + ? flat['_type'].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, + }); + }, +}); + +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 new file mode 100644 index 000000000000..22553e9ee38a --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/unserialize-jsonapi.ts @@ -0,0 +1,23 @@ +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. Included relationships are inlined, and the result stays re-serializable.', + auth: undefined, + props: { + response: Property.Json({ + displayName: 'JSON:API Response', + description: 'The "body" output of the API Call action. Needs a top-level "data" key.', + 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/actions/update-customer-user.ts b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts new file mode 100644 index 000000000000..bd35989a19da --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer-user.ts @@ -0,0 +1,157 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + customerDropdown, + customerUserRolesMultiDropdown, + organizationDropdown, + userDropdown, + websiteDropdown, + booleanUpdateDropdown, + readBooleanUpdate, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, + toHeaderRecord, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +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. Prefer a value from a secret store over a literal one.', + 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: booleanUpdateDropdown({ + displayName: 'Enabled', + description: 'Enable or disable the storefront account.', + }), + confirmed: booleanUpdateDropdown({ + displayName: 'Confirmed', + description: 'Whether the user has completed email confirmation.', + }), + 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: customerUserRolesMultiDropdown, + owner: userDropdown, + organization: organizationDropdown, + 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({ + email: p.email, + firstName: p.firstName, + lastName: p.lastName, + password: p.password, + enabled: readBooleanUpdate(p.enabled), + confirmed: readBooleanUpdate(p.confirmed), + 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, + }; + + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes, + relationships, + actionName: 'Update Customer User', + }); + + const response = await oroApiCall({ + method: HttpMethod.PATCH, + resourceUri: `/customerusers/${p.customerUserId}`, + auth: context.auth, + body: { + data: { + type: 'customerusers', + id: p.customerUserId, + attributes, + relationships, + }, + }, + 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 new file mode 100644 index 000000000000..e647e34adabe --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-customer.ts @@ -0,0 +1,120 @@ +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, + additionalHeadersProp, + toHeaderRecord, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +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, + 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({ + 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, + }; + + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes, + relationships, + actionName: 'Update Customer', + }); + + const response = await oroApiCall({ + method: HttpMethod.PATCH, + resourceUri: `/customers/${p.customerId}`, + auth: context.auth, + body: { + data: { + type: 'customers', + id: p.customerId, + attributes, + relationships, + }, + }, + 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 new file mode 100644 index 000000000000..e09487bac6c7 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/actions/update-user.ts @@ -0,0 +1,169 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, + oroApiCall, + businessUnitDropdown, + businessUnitsMultiDropdown, + organizationDropdown, + organizationsMultiDropdown, + userRolesMultiDropdown, + userGroupsMultiDropdown, + userAuthStatusDropdown, + booleanUpdateDropdown, + readBooleanUpdate, + additionalAttributesProp, + additionalRelationsProp, + additionalHeadersProp, + toHeaderRecord, +} from '../common'; +import { jsonApiBodyUtils } from '../common/jsonapi'; + +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. Prefer a value from a secret store over a literal one.', + 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: booleanUpdateDropdown({ + displayName: 'Enabled', + description: 'Enable or disable the user account.', + }), + + // -- Relationships --------------------------------------------------------- + owner: businessUnitDropdown, + organization: organizationDropdown, + businessUnits: businessUnitsMultiDropdown, + userRoles: userRolesMultiDropdown, + organizations: organizationsMultiDropdown, + groups: userGroupsMultiDropdown, + 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: readBooleanUpdate(p.enabled), + 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, + }; + + jsonApiBodyUtils.assertUpdateNotEmpty({ + attributes, + relationships, + actionName: 'Update User', + }); + + const response = await oroApiCall({ + method: HttpMethod.PATCH, + resourceUri: `/users/${p.userId}`, + auth: context.auth, + body: { + data: { + type: 'users', + id: p.userId, + attributes, + relationships, + }, + }, + headers: toHeaderRecord({ value: p.additionalHeaders }), + }); + + 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..adf273faa5bc --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/address-props.ts @@ -0,0 +1,203 @@ +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, + addr, + extraAttributes = {}, +}: { + lid: string; + type: string; + addr: AddressRow; + extraAttributes?: 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 = {}; + + for (const f of ADDRESS_TEXT_FIELDS) { + if (addr[f]) attrs[f] = addr[f]; + } + if (addr.primary != null) attrs['primary'] = addr.primary; + + const types = buildAddressTypes(addr); + 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, +}: 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 + * 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, + }), +}; + +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 new file mode 100644 index 000000000000..9b75c018a98c --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -0,0 +1,84 @@ +import { PieceAuth, Property } from '@activepieces/pieces-framework'; +import { HttpMethod, HttpResponse, HttpMessageBody } from '@activepieces/pieces-common'; +import { oroApiCall } from './client'; +import { AppConnectionType, tryCatch } from '@activepieces/pieces-framework'; + +export const oroAuth = 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, + }), + headers: Property.LongText({ + displayName: 'Default HTTP Headers', + description: + '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({ + 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 }> => { + const { error } = await tryCatch>(() => + oroApiCall({ + method: HttpMethod.GET, + resourceUri: 'regions/US-CA', + auth: { + type: AppConnectionType.CUSTOM_AUTH, + props: auth, + }, + }), + ); + if (error) { + return { + valid: false, + 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 new file mode 100644 index 000000000000..054ce4e24083 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -0,0 +1,244 @@ +import { createHash } from 'node:crypto'; + +import { + httpClient, + HttpMethod, + HttpMessageBody, + HttpResponse, + HttpError, + AuthenticationType, +} from '@activepieces/pieces-common'; +import { tryCatch } from '@activepieces/pieces-framework'; + +import { + type OroAuth, + type OroAuthResponseType, + type OroApiCallParams, + type OroJsonApiItem, + type OroJsonApiCollection, + type FetchCollectionParams, +} from './types'; +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 createHash('sha256') + .update([getOroServerUrl(auth), auth.props.clientId, auth.props.clientSecret].join('\0')) + .digest('hex'); +} + +export 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)}`; +} + +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; +} + +export 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`; +} + +export 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 inFlight = inFlightTokenRequests.get(cacheKey); + if (inFlight) { + return inFlight; + } + + const request = requestAccessToken({ auth, cacheKey }).finally(() => { + inFlightTokenRequests.delete(cacheKey); + }); + inFlightTokenRequests.set(cacheKey, request); + + 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({ + method, + resourceUri, + auth, + queryParams, + body, + headers: extraHeaders, + throwOriginalError = false +}: OroApiCallParams): Promise> { + const sendRequest = async ({ token }: { token: string }): Promise> => + await httpClient.sendRequest({ + method, + url: `${getOroAdminApiBaseUrl({ auth })}/${resourceUri.replace(/^\/+/, '')}`, + headers: { + 'Content-Type': 'application/vnd.api+json', + ...getConnectionHeaders({ auth }), + ...getInternalInfrastructureHeaders({ auth }), + ...extraHeaders, + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token, + }, + queryParams, + 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) { + if (throwOriginalError) { + throw error; + } else { + throw new Error(formatError({ error })); + } + } +} + +export async function fetchCollection({ + auth, + resourceUri, + queryParams, +}: FetchCollectionParams): Promise { + const response = await oroApiCall({ + method: HttpMethod.GET, + resourceUri, + auth, + queryParams: { 'page[size]': '50', ...queryParams }, + }); + + 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; + 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 { + ...sanitized, + data: jsonApiBodyUtils.omitEmptyObjects(sanitized['data'] as Record), + }; +} + +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 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 new file mode 100644 index 000000000000..9698dfa11c3d --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/index.ts @@ -0,0 +1,6 @@ +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/jsonapi/body-utils.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts new file mode 100644 index 000000000000..ac66b0a3b2cb --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/body-utils.ts @@ -0,0 +1,107 @@ +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 && id !== '' && (!Array.isArray(id) || id.length > 0)) + .map(([key, [type, id, many]]) => [ + key, + Array.isArray(id) + ? { data: id.filter((i) => i !== '').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) { + 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"}}.` + ); + } + 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; +} + +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/src/lib/common/jsonapi/deserialize.ts b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts new file mode 100644 index 000000000000..df2881cdd4ec --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/deserialize.ts @@ -0,0 +1,68 @@ +import type { + JsonApiDocument, + JsonApiResource, + JsonApiResourceDocument, + Linkage, + FlatResource, + DeserializeResult, +} from './types'; + +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}`; + 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) { + out[name] = NULL_RELATIONSHIP; + continue; + } + if (Array.isArray(lnk)) { + 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)); + } + return out; +} + +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)) { + return doc.data.map((r) => flattenResource(r, index, new Set())); + } + 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 new file mode 100644 index 000000000000..6bbc242e62e8 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/index.ts @@ -0,0 +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 new file mode 100644 index 000000000000..69d9ec4e87d9 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/serialize.ts @@ -0,0 +1,212 @@ +import type { FlatResource, JsonApiResource, Linkage, SerializeOptions, SerializeResult } from './types'; + +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; +} + +function hasTypeMarker(value: unknown): value is MarkedResource { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + '_type' in value && + typeof value['_type'] === 'string' + ); +} + +function isNullRelationship(value: unknown): boolean { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + '_type' in value && + value['_type'] === null + ); +} + +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 + ); +} + +function isLinkageLike(value: unknown): value is MarkedResource | RawLinkage { + return hasTypeMarker(value) || isRawLinkage(value); +} + +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)) { + const hoisted: JsonApiResource = { type: linkage.type, id: linkage.id }; + collected.set(key, hoisted); + + 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; +} + +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 = {}; + + for (const [key, value] of Object.entries(flat)) { + if (key === '_type' || key === 'id') continue; + + if (isNullRelationship(value)) { + relationships[key] = { data: null }; + } 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; + } + } + + return { attributes, relationships }; +} + +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()) }; +} + +type MarkedResource = FlatResource & { _type: string }; + +type RawLinkage = { type: string; id: unknown }; + +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 new file mode 100644 index 000000000000..bf3384309ad3 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/jsonapi/types.ts @@ -0,0 +1,72 @@ +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[]; +} + +export interface JsonApiResourceDocument { + data: JsonApiResource; + included?: JsonApiResource[]; +} + +/** + * 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/line-items.ts b/packages/pieces/community/orocommerce/src/lib/common/line-items.ts new file mode 100644 index 000000000000..f69083850b1e --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/line-items.ts @@ -0,0 +1,245 @@ +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(Number((value * 100).toFixed(4))); +} + +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 (!DECIMAL_NUMBER.test(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 (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.` + ); + } + + 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, +}; + +const DECIMAL_NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/; + +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/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts new file mode 100644 index 000000000000..0d2bfddf9625 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -0,0 +1,827 @@ +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'; + +const NOT_CONNECTED = { + disabled: true, + placeholder: 'Connect your OroCommerce account first', + options: [], +}; + +const FAILED = { + disabled: true, + placeholder: + 'Could not load the list. Check the connection and its permissions for this resource.', + options: [], +}; + +const PAGE_SIZE = 100; + +const MAX_PAGES = 20; + +function sanitizeSearch(value: string): string { + return value.trim().replaceAll('"', ''); +} + +export 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)); +} + +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, + required = false, + refreshers = [], + resourceUri, + fieldsParam, + searchExpr, + labelFn, + extraParams = {}, +}: SearchableDropdownConfig) { + return Property.Dropdown({ + auth: oroAuth, + displayName, + description, + required, + refreshers, + refreshOnSearch: true, + options: ({ auth }, { searchValue }) => { + const trimmed = searchValue?.trim() ?? ''; + return loadDropdownOptions({ + auth, + resourceUri, + fieldsParam, + labelFn, + queryParams: { + ...extraParams, + ...(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, + required = false, + resourceUri, + labelFn, + extraParams = {}, +}: EnumDropdownConfig) { + return Property.Dropdown({ + auth: oroAuth, + displayName, + description, + required, + refreshers: [], + options: ({ auth }) => + loadDropdownOptions({ + auth, + resourceUri, + labelFn, + queryParams: extraParams, + exhaustive: true, + }), + }); +} + +// --- 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({ + 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 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}`]; + const trimmed = searchValue?.trim() ?? ''; + if (trimmed.length > 0) { + searchFilters.push(`allText ~ "${sanitizeSearch(trimmed)}"`); + } + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/customerusers', + 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 namePart = [firstName, lastName].filter(Boolean).join(' '); + const label = [namePart, email].filter(Boolean).join(' - ') || item.id; + return { label, value: item.id }; + }), + }; + } catch { + return FAILED; + } + }, + }); + +// --- Organizations ------------------------------------------------------------ + +export const organizationDropdown = makeSearchableDropdown({ + displayName: 'Organization', + description: 'The organization this record belongs to.', + resourceUri: '/organizations', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +// --- 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 = makeSearchableDropdown({ + displayName: 'Website', + description: 'The website this record is associated with.', + resourceUri: '/websites', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +// --- Invoice Internal Statuses ------------------------------------------------ + +export const invoiceInternalStatusDropdown = makeEnumDropdown({ + displayName: 'Internal Status', + description: 'Invoice internal status (e.g. Draft, Open).', + resourceUri: '/invoiceinternalstatuses', + labelFn: attrLabel('name'), +}); + +// --- Order Internal Statuses -------------------------------------------------- + +export const orderInternalStatusDropdown = makeEnumDropdown({ + displayName: 'Internal Status', + description: 'Order internal status (e.g. Open, Cancelled).', + 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, + 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]': 'id,sku,status,names', + include: 'names', + 'fields[productnames]': 'id,string,localization,product', + }; + const trimmed = searchValue?.trim() ?? ''; + if (trimmed.length > 0) { + params['filter[searchQuery]'] = `allText ~ "${sanitizeSearch( + trimmed + )}" and productStatus = "enabled"`; + } else { + params['filter[status]'] = 'enabled'; + } + + 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; + }[]; + }; + + const nameMap = buildProductNameMap(body.included ?? []); + + return { + options: (body.data ?? []).map((item) => { + const sku = String(item.attributes['sku'] ?? ''); + const name = nameMap[item.id] ?? ''; + return { + label: name ? `${item.id}: ${sku} - ${name}` : `${item.id}: ${sku}`, + value: item.id, + }; + }), + }; + } catch { + return FAILED; + } + }, +}); + +// --- Payment Terms ------------------------------------------------------------ + +export const paymentTermDropdown = makeSearchableDropdown({ + displayName: 'Payment Term', + description: 'Search payment terms.', + resourceUri: '/paymentterms', + fieldsParam: 'id,label', + searchExpr: (q) => `label ~ "${q}"`, + labelFn: attrLabel('label'), +}); + +// --- Warehouses --------------------------------------------------------------- + +export const warehouseDropdown = makeSearchableDropdown({ + displayName: 'Warehouse', + description: 'Search warehouses.', + resourceUri: '/warehouses', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); + +// --- Countries ---------------------------------------------------------------- +// Loads the full list (~250 ISO countries) once and filters client-side. + +export const buildCountryDropdown = ( + { required = false, displayName = 'Country' }: { required?: boolean; displayName?: string } = {} +) => + 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 items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/countries', + queryParams: { 'fields[countries]': 'id,name', 'page[size]': '300' }, + }); + return { + options: clientSideFilterAndSort({ + items, + searchValue, + labelAttr: 'name', + }), + }; + } catch { + return FAILED; + } + }, + }); + +export const buildRegionDropdown = ({ + countryRefresher, + required = false, + displayName = 'Region / State', +}: { + countryRefresher: string; + required?: boolean; + displayName?: string; +}) => + Property.Dropdown({ + auth: oroAuth, + displayName, + description: + 'Region or state. Select a country first. Start typing to filter.', + required, + refreshers: [countryRefresher], + options: async ({ auth, ...refreshed }) => { + const countryId = refreshed[countryRefresher]; + if (!auth) return NOT_CONNECTED; + if (typeof countryId !== 'string' || countryId.length === 0) + return { + disabled: true, + placeholder: 'Select a country first', + options: [], + }; + 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)), + }; + }, + }); + +// --- Order Statuses (external) ------------------------------------------------ + +export const orderStatusDropdown = makeEnumDropdown({ + displayName: 'Status', + description: + 'Order status managed by an external system (only relevant when "Enable External Status Management" is on).', + resourceUri: '/orderstatuses', + labelFn: attrLabel('name'), +}); + +// --- Orders ------------------------------------------------------------------- + +export const orderDropdown = makeSearchableDropdown({ + displayName: 'Parent Order', + description: 'Search orders to use as the parent order.', + resourceUri: '/orders', + fieldsParam: 'id,identifier,poNumber', + searchExpr: (q) => `allText ~ "${q}"`, + labelFn: attrLabel('identifier', 'poNumber'), +}); + +// --- Product Units ------------------------------------------------------------ + +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'), +}); + +// --- 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'), +}); + +// --- 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 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.', + 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'), +}); + +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({ + displayName: 'User Group', + description: 'Back-office user group. Search by name.', + resourceUri: '/usergroups', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + 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({ + displayName: 'Organizations', + description: 'Organizations the user has access to. Search by name.', + resourceUri: '/organizations', + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + 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({ + 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({ + 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'), +}); + +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'), +}); + +// --- 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({ + displayName: 'Additional Attributes', + description: 'Custom fields, merged after the standard ones. Example: {"myField": "value"}', + required: false, + defaultValue: {}, +}); + +export const additionalRelationsProp = Property.Json({ + displayName: 'Additional Relations', + description: + 'Custom relationships in linkage format. Example: {"myRelation": {"data": {"type": "myentities", "id": "1"}}}', + required: false, + defaultValue: {}, +}); + +export const additionalHeadersProp = Property.Object({ + displayName: 'Additional Headers', + description: 'Headers for this step; override the connection defaults. Example: {"X-Include": "totalCount"}', + required: false, + defaultValue: {}, +}); + +// --- Private helpers ---------------------------------------------------------- + +type BooleanUpdateValue = typeof LEAVE_UNCHANGED | 'true' | 'false'; + +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 CollectionPages = { + items: OroJsonApiItem[]; + complete: boolean; +}; + +type SearchableDropdownConfig = { + displayName: string; + description: string; + required?: boolean; + refreshers?: string[]; + resourceUri: string; + fieldsParam: string; + searchExpr: (sanitizedQuery: string) => string; + labelFn: LabelFn; + extraParams?: Record; +}; + +type MultiSelectDropdownConfig = Omit; + +type EnumDropdownConfig = { + displayName: string; + description: string; + required?: boolean; + resourceUri: string; + labelFn: LabelFn; + extraParams?: Record; +}; 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..b9b071bfde24 --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -0,0 +1,43 @@ +import { + HttpMessageBody, + HttpMethod, + QueryParams, +} from '@activepieces/pieces-common'; + +import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework'; + +import { oroAuth } from './auth'; + +export type OroAuth = AppConnectionValueForAuthProperty; + +export type OroAuthResponseType = { + token_type: string; + access_token: string; + expires_in: number; +}; + +export type OroApiCallParams = { + method: HttpMethod; + resourceUri: string; + auth: OroAuth; + queryParams?: QueryParams; + body?: HttpMessageBody; + headers?: Record; + throwOriginalError?: boolean; +}; + +export type FetchCollectionParams = { + auth: OroAuth; + resourceUri: string; + queryParams?: Record; +}; + +export interface OroJsonApiItem { + id: string; + type: string; + attributes: Record; +} + +export interface OroJsonApiCollection { + data: OroJsonApiItem[]; +} 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..ecb617d610ba --- /dev/null +++ b/packages/pieces/community/orocommerce/src/lib/triggers/webhook-topic-trigger.ts @@ -0,0 +1,228 @@ +import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; + +import { + createTrigger, + Property, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { oroAuth, oroApiCall, formatError } from '../common'; +import { OroAuth, OroJsonApiCollection, OroJsonApiItem } from '../common/types'; +import { HttpError, 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'], + 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, + })), + }; + }, + }), + 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.', + required: false, + defaultValue: true, + }), + }, + type: TriggerStrategy.WEBHOOK, + sampleData: {}, + + async onEnable(context) { + // 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) { + 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', + auth: context.auth, + body: { + data: { + type: 'webhooks', + attributes: { + enabled: true, + notificationUrl: context.webhookUrl, + ...(secret === undefined ? {} : { secret }), + }, + relationships: { + topic: { + data: { + type: 'webhooktopics', + id: context.propsValue.topic, + }, + }, + format: { + data: { + type: 'webhookformats', + id: 'default', + }, + }, + }, + }, + }, + }); + + 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.'); + } + + 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) { + 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 ?? 'unknown'}, step "${context.step?.name ?? 'unknown'}"): ${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/action-guards.test.ts b/packages/pieces/community/orocommerce/test/action-guards.test.ts new file mode 100644 index 000000000000..98fbae3afbb2 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/action-guards.test.ts @@ -0,0 +1,213 @@ +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'; + +// 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', + 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', unitOfQuantity: 'piece', 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', 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, 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, unitOfQuantity: 'piece', 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('create invoice refuses to file a non-PDF as the invoice PDF', () => { + 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: content, + invoicePdfFilename: filename, + }, + }) + ); + } + + it('names the file when the content is not a PDF', async () => { + await expect( + 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 () => { + await expect( + 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'); + }); +}); + +// 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( + 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.' + ); + }); +}); 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..d358feb2cae4 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/body-utils.test.ts @@ -0,0 +1,162 @@ +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/ + ); + }); + + // 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', () => { + 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/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', + ]); + }); +}); 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/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/test/line-items.test.ts b/packages/pieces/community/orocommerce/test/line-items.test.ts new file mode 100644 index 000000000000..f06f20abe7e4 --- /dev/null +++ b/packages/pieces/community/orocommerce/test/line-items.test.ts @@ -0,0 +1,292 @@ +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('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({ + 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 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 } }) + ).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('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' }) + ).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/); + }); +}); 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..6c1459342e9b --- /dev/null +++ b/packages/pieces/community/orocommerce/test/webhook-signature.test.ts @@ -0,0 +1,349 @@ +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('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' }); + + 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'); + }); +}); 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..e0e32be985e2 --- /dev/null +++ b/packages/pieces/community/orocommerce/tools/check-i18n.mjs @@ -0,0 +1,267 @@ +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'); + 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.`); + } + 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)) }); + 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; + } + + 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/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/packages/pieces/community/orocommerce/tsconfig.json b/packages/pieces/community/orocommerce/tsconfig.json new file mode 100644 index 000000000000..07e0ec6eb797 --- /dev/null +++ b/packages/pieces/community/orocommerce/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.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..0ba4caeb858b --- /dev/null +++ b/packages/pieces/community/orocommerce/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "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"] +} 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..ba8ade4a1780 --- /dev/null +++ b/packages/pieces/community/orocommerce/vitest.config.ts @@ -0,0 +1,17 @@ +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/pieces-framework': path.resolve(repoRoot, 'packages/pieces/framework/src/index.ts'), + '@activepieces/pieces-common': path.resolve(repoRoot, 'packages/pieces/common/src/index.ts'), + }, + }, +})