Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions components/fiserv/actions/create-checkout/create-checkout.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import app from "../../fiserv.app.mjs";
import utils from "../../common/utils.mjs";

export default {
key: "fiserv-create-checkout",
name: "Create Checkout",
description: "Initiate a payment request by passing all the required parameters. It creates a new payment transaction and returns the redirect URL that includes transaction ID. [See the documentation](https://docs.fiserv.dev/public/reference/postcheckouts).",
version: "0.0.1",
type: "action",
props: {
app,
storeId: {
type: "string",
label: "Store ID",
description: "Store id to be used for processing this payment. It also acts as an identifier for your store to load the checkout pages linked to it. If no checkout pages are found, default payment page will be rendered for that transaction.",
},
merchantTransactionId: {
type: "string",
label: "Merchant Transaction ID",
description: "You can use this parameter to tag a unique identifier to this transaction for future reference.",
optional: true,
},
transactionOrigin: {
type: "string",
label: "Transaction Origin",
description: "This parameter is used to flag the transaction source correctly. The possible values are `ECOM` (if the order was recieved from online shop), `MAIL` & `PHONE`.",
options: [
"ECOM",
"MAIL",
"PHONE",
],
},
transactionType: {
type: "string",
label: "Transaction Type",
description: "You can use this parameter to specify the type of transaction you want to perform. Allowed values are: `SALE`, `PRE-AUTH`, `ZERO-AUTH`",
options: [
"SALE",
"PRE-AUTH",
"ZERO-AUTH",
],
},
transactionAmount: {
type: "object",
label: "Transaction Amount",
description: "Object contains `total` transaction amount, `currency`, tax and discount details. Example: `{\"total\":123,\"currency\":\"EUR\",\"components\":{\"subtotal\":115,\"vat\":3,\"shipping\":2.5}}`",
optional: true,
},
Comment on lines +43 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Inconsistent handling of 'transactionAmount' prop type and unnecessary JSON parsing.

The transactionAmount prop is declared as type object, but in the run method (line 116), it is parsed using utils.parseJson(transactionAmount). Since transactionAmount is already an object, parsing it as JSON is unnecessary and may cause errors if the object is not serializable. Consider either changing the prop type to string if you expect JSON input as a string, or removing the JSON parsing if an object is expected.

If you expect transactionAmount to be an object, apply this diff:

- transactionAmount: utils.parseJson(transactionAmount),
+ transactionAmount: transactionAmount,

Alternatively, if you expect input as a JSON string, change the prop type to string:

- transactionAmount: {
-   type: "object",
+ transactionAmount: {
+   type: "string",

Committable suggestion skipped: line range outside the PR's diff.

order: {
type: "string",
label: "Order",
description: "Object contains order related details. [See the documentation](https://docs.fiserv.dev/public/reference/postcheckouts).",
optional: true,
},
Comment on lines +49 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Inconsistent type definition for order prop.

The order prop is defined as type string but the documentation indicates it should be an object. This creates confusion and requires unnecessary JSON parsing. Consider changing the type to object to maintain consistency with the API expectations.

Apply this diff:

 order: {
-  type: "string",
+  type: "object",
   label: "Order",
   description: "Object contains order related details. [See the documentation](https://docs.fiserv.dev/public/reference/postcheckouts).",
   optional: true,
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
order: {
type: "string",
label: "Order",
description: "Object contains order related details. [See the documentation](https://docs.fiserv.dev/public/reference/postcheckouts).",
optional: true,
},
order: {
type: "object",
label: "Order",
description: "Object contains order related details. [See the documentation](https://docs.fiserv.dev/public/reference/postcheckouts).",
optional: true,
},

checkoutSettings: {
type: "string",
label: "Checkout Settings",
description: "Object contains checkout related settings. [See the documentation](https://docs.fiserv.dev/public/reference/postcheckouts).",
default: JSON.stringify({
locale: "en_US",
}),
},
paymentMethodDetails: {
type: "string",
label: "Payment Method Details",
description: "Object contains payment method related details. [See the documentation](https://docs.fiserv.dev/public/reference/postcheckouts).",
default: JSON.stringify({
cards: {
authenticationPreferences: {
challengeIndicator: "01",
skipTra: false,
},
createToken: {
declineDuplicateToken: false,
reusable: true,
toBeUsedFor: "UNSCHEDULED",
},
tokenBasedTransaction: {
transactionSequence: "FIRST",
},
},
sepaDirectDebit: {
transactionSequenceType: "SINGLE",
},
}),
},
},
methods: {
createCheckout(args = {}) {
return this.app.post({
path: "/checkouts",
...args,
});
},
},
async run({ $ }) {
const {
createCheckout,
storeId,
merchantTransactionId,
transactionOrigin,
transactionType,
transactionAmount,
order,
checkoutSettings,
paymentMethodDetails,
} = this;

const response = await createCheckout({
$,
data: {
storeId,
merchantTransactionId,
transactionOrigin,
transactionType,
transactionAmount: utils.parseJson(transactionAmount),
order: utils.parseJson(order),
checkoutSettings: utils.parseJson(checkoutSettings),
paymentMethodDetails: utils.parseJson(paymentMethodDetails),
},
});
Comment on lines +109 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add response validation to ensure required fields are present.

The Fiserv API response should be validated to ensure it contains the expected fields before using them.

Add validation before using the response:

 const response = await createCheckout({
   $,
   data: {
     storeId,
     merchantTransactionId,
     transactionOrigin,
     transactionType,
     transactionAmount: utils.parseJson(transactionAmount),
     order: utils.parseJson(order),
     checkoutSettings: utils.parseJson(checkoutSettings),
     paymentMethodDetails: utils.parseJson(paymentMethodDetails),
   },
 });
+
+if (!response?.redirectionUrl) {
+  throw new Error('Invalid response: missing redirectionUrl');
+}

Committable suggestion skipped: line range outside the PR's diff.


$.export("$summary", `Successfully created checkout. Redirect URL: ${response.redirectionUrl}`);
return response;
},
Comment on lines +96 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure proper error handling for JSON parsing and API requests.

When parsing JSON inputs (transactionAmount, order, checkoutSettings, paymentMethodDetails), consider handling potential parsing errors to prevent the application from crashing due to invalid JSON input. Additionally, ensure that errors from the createCheckout API request are properly caught and handled to inform the user appropriately.

Consider wrapping the parsing calls with try-catch blocks or using a safe parsing method that returns meaningful errors if parsing fails.

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import app from "../../fiserv.app.mjs";

export default {
key: "fiserv-retrieve-checkout-details",
name: "Retrieve Checkout Details",
description: "Retrieve details about a specific checkout using the identifier returned when it was created. [See the documentation](https://docs.fiserv.dev/public/reference/get-checkouts-id).",
version: "0.0.1",
type: "action",
props: {
app,
checkoutId: {
type: "string",
label: "Checkout ID",
description: "The unique identifier for the checkout.",
optional: false,
},
},
methods: {
getCheckoutDetails({
checkoutId, ...args
} = {}) {
return this.app._makeRequest({
path: `/checkouts/${checkoutId}`,
...args,
});
},
},
Comment on lines +18 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and error handling

The current implementation could be more robust. Consider these improvements:

  1. Validate checkoutId before making the request
  2. Add error handling for edge cases
  3. Be more explicit about which parameters can be overridden via args
 methods: {
   getCheckoutDetails({
     checkoutId, ...args
   } = {}) {
+    if (!checkoutId || typeof checkoutId !== 'string') {
+      throw new Error('Invalid checkoutId provided');
+    }
+    const allowedParams = ['$'];
+    const invalidParams = Object.keys(args).filter(key => !allowedParams.includes(key));
+    if (invalidParams.length > 0) {
+      throw new Error(`Invalid parameters provided: ${invalidParams.join(', ')}`);
+    }
     return this.app._makeRequest({
       path: `/checkouts/${checkoutId}`,
-      ...args,
+      $: args.$,
     });
   },
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
methods: {
getCheckoutDetails({
checkoutId, ...args
} = {}) {
return this.app._makeRequest({
path: `/checkouts/${checkoutId}`,
...args,
});
},
},
methods: {
getCheckoutDetails({
checkoutId, ...args
} = {}) {
if (!checkoutId || typeof checkoutId !== 'string') {
throw new Error('Invalid checkoutId provided');
}
const allowedParams = ['$'];
const invalidParams = Object.keys(args).filter(key => !allowedParams.includes(key));
if (invalidParams.length > 0) {
throw new Error(`Invalid parameters provided: ${invalidParams.join(', ')}`);
}
return this.app._makeRequest({
path: `/checkouts/${checkoutId}`,
$: args.$,
});
},
},

async run({ $ }) {
const {
getCheckoutDetails,
checkoutId,
} = this;

const response = await getCheckoutDetails({
$,
checkoutId,
});

$.export("$summary", `Successfully retrieved details for checkout ID ${this.checkoutId}`);
return response;
},
Comment on lines +28 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and fix inconsistent property access

The run function needs several improvements:

  1. Fix inconsistent use of this.checkoutId vs checkoutId
  2. Add error handling for API calls
  3. Add response validation
 async run({ $ }) {
   const {
     getCheckoutDetails,
     checkoutId,
   } = this;

+  try {
     const response = await getCheckoutDetails({
       $,
       checkoutId,
     });
 
-    $.export("$summary", `Successfully retrieved details for checkout ID ${this.checkoutId}`);
+    if (!response || typeof response !== 'object') {
+      throw new Error('Invalid response received from API');
+    }
+
+    $.export("$summary", `Successfully retrieved details for checkout ID ${checkoutId}`);
     return response;
+  } catch (error) {
+    throw new Error(`Failed to retrieve checkout details: ${error.message}`);
+  }
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async run({ $ }) {
const {
getCheckoutDetails,
checkoutId,
} = this;
const response = await getCheckoutDetails({
$,
checkoutId,
});
$.export("$summary", `Successfully retrieved details for checkout ID ${this.checkoutId}`);
return response;
},
async run({ $ }) {
const {
getCheckoutDetails,
checkoutId,
} = this;
try {
const response = await getCheckoutDetails({
$,
checkoutId,
});
if (!response || typeof response !== 'object') {
throw new Error('Invalid response received from API');
}
$.export("$summary", `Successfully retrieved details for checkout ID ${checkoutId}`);
return response;
} catch (error) {
throw new Error(`Failed to retrieve checkout details: ${error.message}`);
}
},

};
18 changes: 18 additions & 0 deletions components/fiserv/common/constants.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const ENVIRONMENT = {
SANDBOX: "sandbox",
PRODUCTION: "production",
};

const SANDBOX_PATH = "/sandbox";

const API_PATH = {
DEFAULT: "/exp/v1",
PAYMENTS: "/ipp/payments-gateway/v2",
FRAUD: "/ipp/fraud/v1",
};

export default {
ENVIRONMENT,
SANDBOX_PATH,
API_PATH,
};
26 changes: 26 additions & 0 deletions components/fiserv/common/utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const parseJson = (input) => {
const parse = (value) => {
if (typeof(value) === "string") {
try {
return parseJson(JSON.parse(value));
} catch (e) {
return value;
}
} else if (typeof(value) === "object" && value !== null) {
Comment on lines +3 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add safety measures to prevent recursion-related issues.

The current implementation has potential security and stability issues:

  1. No maximum recursion depth limit could lead to stack overflow with deeply nested JSON.
  2. Silent error handling could mask important parsing issues.

Consider implementing these safety measures:

+const MAX_DEPTH = 100; // Prevent excessive recursion
-const parse = (value) => {
+const parse = (value, depth = 0) => {
+  if (depth > MAX_DEPTH) {
+    throw new Error('Maximum parsing depth exceeded');
+  }
   if (typeof(value) === "string") {
     try {
-      return parseJson(JSON.parse(value));
+      return parse(JSON.parse(value), depth + 1);
     } catch (e) {
+      // Log parsing errors in development
+      if (process.env.NODE_ENV === 'development') {
+        console.warn('JSON parsing failed:', e.message);
+      }
       return value;
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof(value) === "string") {
try {
return parseJson(JSON.parse(value));
} catch (e) {
return value;
}
} else if (typeof(value) === "object" && value !== null) {
const MAX_DEPTH = 100; // Prevent excessive recursion
if (typeof(value) === "string") {
try {
return parse(JSON.parse(value), depth + 1);
} catch (e) {
// Log parsing errors in development
if (process.env.NODE_ENV === 'development') {
console.warn('JSON parsing failed:', e.message);
}
return value;
}
} else if (typeof(value) === "object" && value !== null) {

return Object.entries(value)
.reduce((acc, [
key,
val,
]) => Object.assign(acc, {
[key]: parse(val),
}), {});
}
Comment on lines +9 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve object handling robustness and efficiency.

The current implementation has several areas for improvement:

  1. No explicit handling of arrays
  2. No protection against circular references
  3. Object reconstruction could be more efficient

Consider this enhanced implementation:

-} else if (typeof(value) === "object" && value !== null) {
+} else if (value && typeof(value) === "object") {
+  // Handle circular references
+  if (seen.has(value)) {
+    throw new Error('Circular reference detected');
+  }
+  seen.add(value);
+
+  // Handle arrays
+  if (Array.isArray(value)) {
+    return value.map(item => parse(item, depth + 1));
+  }
+
+  // Handle objects more efficiently
+  const result = {};
+  for (const [key, val] of Object.entries(value)) {
+    result[key] = parse(val, depth + 1);
+  }
+  seen.delete(value);
+  return result;
-  return Object.entries(value)
-    .reduce((acc, [
-      key,
-      val,
-    ]) => Object.assign(acc, {
-      [key]: parse(val),
-    }), {});

Committable suggestion skipped: line range outside the PR's diff.

return value;
};

return parse(input);
};

export default {
parseJson,
};
92 changes: 87 additions & 5 deletions components/fiserv/fiserv.app.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,93 @@
import { v4 as uuid } from "uuid";
import crypto from "crypto";
import { axios } from "@pipedream/platform";
import constants from "./common/constants.mjs";

export default {
type: "app",
app: "fiserv",
propDefinitions: {},
methods: {
// this.$auth contains connected account data
authKeys() {
console.log(Object.keys(this.$auth));
getAuth() {
const {
url,
api_key: apiKey,
secret_key: secretKey,
environment = constants.ENVIRONMENT.SANDBOX,
} = this.$auth;
return {
url,
apiKey,
secretKey,
environment,
};
},
Comment on lines +10 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for required authentication properties.

While the method correctly extracts auth properties, it should validate that required properties (url, api_key, secret_key) are present to fail fast and provide clear error messages.

Consider adding validation:

 getAuth() {
   const {
     url,
     api_key: apiKey,
     secret_key: secretKey,
     environment = constants.ENVIRONMENT.SANDBOX,
   } = this.$auth;
+  if (!url || !apiKey || !secretKey) {
+    throw new Error('Missing required authentication properties: url, api_key, and secret_key must be provided');
+  }
   return {
     url,
     apiKey,
     secretKey,
     environment,
   };
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getAuth() {
const {
url,
api_key: apiKey,
secret_key: secretKey,
environment = constants.ENVIRONMENT.SANDBOX,
} = this.$auth;
return {
url,
apiKey,
secretKey,
environment,
};
},
getAuth() {
const {
url,
api_key: apiKey,
secret_key: secretKey,
environment = constants.ENVIRONMENT.SANDBOX,
} = this.$auth;
if (!url || !apiKey || !secretKey) {
throw new Error('Missing required authentication properties: url, api_key, and secret_key must be provided');
}
return {
url,
apiKey,
secretKey,
environment,
};
},

getUrl(path, apiPath = constants.API_PATH.DEFAULT) {
const {
url,
environment,
} = this.getAuth();
const baseUrl = environment === constants.ENVIRONMENT.SANDBOX
? `${url}${constants.SANDBOX_PATH}${apiPath}`
: `${url}${apiPath}`;
return `${baseUrl}${path}`;
},
/**
* Example at https://docs.fiserv.dev/public/docs/message-signature#example-of-code
*/
getSignatureHeaders(data) {
const {
apiKey,
secretKey,
environment,
} = this.getAuth();

if (environment === constants.ENVIRONMENT.SANDBOX) {
return;
}

const clientRequestId = uuid();
const timestamp = Date.now().toString();
const requestBody = JSON.stringify(data) || "";
const rawSignature = apiKey + clientRequestId + timestamp + requestBody;

const computedHmac =
crypto.createHmac("sha256", secretKey)
.update(rawSignature)
.digest("base64");

return {
"Client-Request-Id": clientRequestId,
"Message-Signature": computedHmac,
"Timestamp": timestamp,
};
},
Comment on lines +37 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure getSignatureHeaders returns an object to prevent errors

In the getSignatureHeaders(data) method, when the environment is set to SANDBOX, the function returns undefined due to return; on line 45. Later, in the _makeRequest method (line 82), spreading undefined into the headers object will cause a runtime error. To prevent this, ensure that getSignatureHeaders always returns an object, even if it's empty.

Apply this diff to fix the issue:

          if (environment === constants.ENVIRONMENT.SANDBOX) {
-            return;
+            return {};
          }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getSignatureHeaders(data) {
const {
apiKey,
secretKey,
environment,
} = this.getAuth();
if (environment === constants.ENVIRONMENT.SANDBOX) {
return;
}
const clientRequestId = uuid();
const timestamp = Date.now().toString();
const requestBody = JSON.stringify(data) || "";
const rawSignature = apiKey + clientRequestId + timestamp + requestBody;
const computedHmac =
crypto.createHmac("sha256", secretKey)
.update(rawSignature)
.digest("base64");
return {
"Client-Request-Id": clientRequestId,
"Message-Signature": computedHmac,
"Timestamp": timestamp,
};
},
getSignatureHeaders(data) {
const {
apiKey,
secretKey,
environment,
} = this.getAuth();
if (environment === constants.ENVIRONMENT.SANDBOX) {
return {};
}
const clientRequestId = uuid();
const timestamp = Date.now().toString();
const requestBody = JSON.stringify(data) || "";
const rawSignature = apiKey + clientRequestId + timestamp + requestBody;
const computedHmac =
crypto.createHmac("sha256", secretKey)
.update(rawSignature)
.digest("base64");
return {
"Client-Request-Id": clientRequestId,
"Message-Signature": computedHmac,
"Timestamp": timestamp,
};
},

getHeaders(headers) {
return {
...headers,
"Content-Type": "application/json",
"Accept": "application/json",
"API-Key": this.$auth.api_key,
};
},
_makeRequest({
$ = this, path, headers, data, apiPath, ...args
} = {}) {
return axios($, {
...args,
debug: true,
url: this.getUrl(path, apiPath),
data,
headers: {
...this.getHeaders(headers),
...this.getSignatureHeaders(data),
},
});
},
post(args = {}) {
return this._makeRequest({
method: "POST",
...args,
});
},
},
};
};
9 changes: 7 additions & 2 deletions components/fiserv/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/fiserv",
"version": "0.0.1",
"version": "0.1.0",
"description": "Pipedream Fiserv Components",
"main": "fiserv.app.mjs",
"keywords": [
Expand All @@ -11,5 +11,10 @@
"author": "Pipedream <[email protected]> (https://pipedream.com/)",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@pipedream/platform": "3.0.3",
"crypto": "^1.0.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unnecessary crypto dependency

The crypto module is a built-in Node.js module and should not be listed as an external dependency. It's available by default in Node.js environments.

Apply this diff to remove the unnecessary dependency:

  "dependencies": {
    "@pipedream/platform": "3.0.3",
-   "crypto": "^1.0.1",
    "uuid": "^11.0.3"
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"crypto": "^1.0.1",

"uuid": "^11.0.3"
}
}
}
Loading
Loading