Skip to content

Commit 1f5b8a5

Browse files
authored
Merge branch 'main' into main
2 parents 607ef15 + 695f7a9 commit 1f5b8a5

File tree

4 files changed

+223
-2
lines changed

4 files changed

+223
-2
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Be sure to check the README for each function in the functions folder, as they p
1717
- Datastore integrations:
1818
- [datastore-integration-webflow](src/functions/datastore-integration-webflow): Validates the price and/or availability of items against Webflow CMS collections before a payment is processed.
1919
- [datastore-integration-orderdesk](src/functions/datastore-integration-orderdesk): Validates the cart against OrderDesk and updates the inventory upon successful transaction.
20+
- [datastore-integration-wix](src/functions/datastore-integration-wix): Validates the price and/or availability of items against Wix Stores before a payment is processed.
2021
- Other features:
2122
- [cart](src/functions/cart): Converts a cart between recurring and non-recurring. Useful in an upsell flow.
2223
- [idevaffiliate-marketplace](src/functions/idevaffiliate-marketplace): A marketplace-style integration, using iDevAffiliate.

config.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ const config = {
3030
storeId: env("FOXY_ORDERDESK_STORE_ID"),
3131
},
3232
webflow: {
33-
collection: env("FOXY_WEBFLOW_COLLECTION"),
34-
token: env("FOXY_WEBFLOW_TOKEN") || env("WEBFLOW_TOKEN"),
33+
collection: env('FOXY_WEBFLOW_COLLECTION'),
34+
token: env('FOXY_WEBFLOW_TOKEN') || env('WEBFLOW_TOKEN'),
3535
},
36+
wix: {
37+
accountId: env("FOXY_WIX_ACCOUNT_ID"),
38+
apiKey: env("FOXY_WIX_API_KEY"),
39+
siteId: env("FOXY_WIX_SITE_ID"),
40+
}
3641
},
3742
skipUpdate: {
3843
inventory: env("FOXY_SKIP_INVENTORY_UPDATE_CODES"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Pre-checkout webhook for Wix-Foxy
2+
3+
This pre-checkout webhook provides security against HTML modifications on your Wix site. It is triggered when a Foxy checkout request is submitted and validates all items in cart to ensure they have sufficient inventory and matched pricing in your Wix store.
4+
5+
It requires using Wix Stores to manage products and integrating with Foxy by following [this tutorial](https://foxy.io/help/articles/use-wix-stores-to-manage-foxy-products).
6+
7+
## Usage
8+
9+
### Get Wix API and account credentials
10+
11+
#### Wix API Key
12+
13+
1. Go to [Wix API Keys Manager](https://manage.wix.com/account/api-keys)
14+
2. Click the **Generate API Key** button
15+
3. Enter a name for the API key
16+
4. For Permissions, expand All site permissions by clicking the **See all** button
17+
5. Check the checkbox for **Wix Stores**
18+
6. Click the **Generate Key** button
19+
7. Copy the API key token and store it somewhere safe
20+
21+
#### Wix Account ID
22+
23+
After generating an API key, in your [Wix API Keys Manager](https://manage.wix.com/account/api-keys), you should see your Wix account ID on the right.
24+
25+
#### Wix Site ID
26+
27+
1. Go to your Wix dashboard and select your site
28+
2. Get the site ID from the URL, which appears after `/dashboard/` in the URL:
29+
![](https://wixmp-833713b177cebf373f611808.wixmp.com/images/about-api-keys-md_media_siteid.png)
30+
31+
### Deploy this repository to Netlify
32+
33+
1. Click the **Fork** button at the top right corner of this page
34+
2. Go to [Netlify](https://www.netlify.com/) and log in your account
35+
3. Add a new site and select the **Import an existing project** option
36+
4. Connect your GitHub account
37+
5. Select repository `foxy-node-netlify-functions`
38+
6. On the review configuration page, under the Environment variables section, click the **Add environment variables** button to add the following variables:
39+
| Key | Value |
40+
| ----------- | ----------- |
41+
| FOXY_WIX_API_KEY | Your Wix API key token |
42+
| FOXY_WIX_ACCOUNT_ID | Your Wix account ID |
43+
| FOXY_WIX_SITE_ID | Your Wix site ID |
44+
7. Click the **Deploy foxy-node-netlify-functions** button
45+
46+
### Configure the pre-checkout webhook in Foxy
47+
48+
1. After the site is deployed successfully on Netlify, click **Logs** > **Functions** from the navigation bar on the left
49+
2. Choose the `datastore-integration-wix` function from the list
50+
3. On the function logs page, copy the endpoint URL
51+
4. Log in your [Foxy Admin](https://admin.foxy.io/)
52+
5. Go to **Settings** > **Payments** and choose the payment set that the pre-checkout webhook should be applied to
53+
6. Click the **Add fraud protection** button and choose **Pre-Checkout Webhook**
54+
7. Toggle **Enabled**
55+
8. In the **URL** field, paste the Netlify function endpoint URL from step 3
56+
9. Select an option for the Failure handling setting
57+
10. Click the **Add fraud protection** button
58+
11. Run some test transactions to ensure the pre-checkout webhook is configured correctly
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
const fetch = require("node-fetch");
2+
const { config } = require("../../../config.js");
3+
4+
/**
5+
* Receives the request, validates the price and inventory in Wix and sends the response.
6+
*
7+
* @param {Object} requestEvent the request event built by Netlify Functions
8+
* @returns {Promise<{statusCode: number, body: string}>} the response object
9+
*/
10+
async function handler(requestEvent) {
11+
const cart = JSON.parse(requestEvent.body);
12+
13+
console.log(`Starting up for transaction ${cart.id}`);
14+
15+
const items = cart["_embedded"]["fx:items"];
16+
const mismatchedPrice = [];
17+
const insufficientInventory = [];
18+
19+
try {
20+
for (const item of items) {
21+
const slug = item["_embedded"]["fx:item_options"].find(
22+
(option) => option.name.toLowerCase() === "slug"
23+
)?.value;
24+
if (!slug) {
25+
const details = `Cannot find slug in item options for item ${item.name}`;
26+
console.error(details);
27+
return {
28+
body: JSON.stringify({
29+
details,
30+
ok: false,
31+
}),
32+
statusCode: 200,
33+
};
34+
}
35+
36+
const wixCreds = config.datastore.provider.wix;
37+
const res = await fetch(
38+
"https://www.wixapis.com/stores-reader/v1/products/query",
39+
{
40+
body: JSON.stringify({
41+
includeVariants: true,
42+
query: {
43+
filter: JSON.stringify({ slug }),
44+
},
45+
}),
46+
headers: {
47+
Authorization: wixCreds.apiKey,
48+
"Content-Type": "application/json",
49+
"wix-account-id": wixCreds.accountId,
50+
"wix-site-id": wixCreds.siteId,
51+
},
52+
method: "POST",
53+
}
54+
);
55+
const data = await res.json();
56+
57+
if (data.totalResults !== 1) {
58+
const details = `Cannot find product in Wix by slug ${slug}`;
59+
console.error(details);
60+
return {
61+
body: JSON.stringify({
62+
details,
63+
ok: false,
64+
}),
65+
statusCode: 200,
66+
};
67+
}
68+
69+
const product = data.products[0];
70+
const variant = product.variants.find(
71+
(variant) => variant.variant.sku === item.code
72+
);
73+
74+
if (!variant) {
75+
const details = `Cannot find variant by sku ${item.code}`;
76+
console.error(details);
77+
return {
78+
body: JSON.stringify({
79+
details,
80+
ok: false,
81+
}),
82+
statusCode: 200,
83+
};
84+
}
85+
86+
const skipPriceCodes =
87+
(config.datastore.skipValidation.price || "")
88+
.split(",")
89+
.map((code) => code.trim())
90+
.filter((code) => !!code) || [];
91+
const skipInventoryCodes =
92+
(config.datastore.skipValidation.inventory || "")
93+
.split(",")
94+
.map((code) => code.trim())
95+
.filter((code) => !!code) || [];
96+
97+
if (!skipPriceCodes.includes(item.code)) {
98+
const price = variant.variant.priceData.discountedPrice;
99+
if (price !== item.price) {
100+
mismatchedPrice.push(item.code);
101+
}
102+
}
103+
104+
if (!skipInventoryCodes.includes(item.code)) {
105+
const stock = variant.stock;
106+
if (stock.trackQuantity) {
107+
if (stock.quantity < item.quantity) {
108+
insufficientInventory.push(item.code);
109+
}
110+
} else {
111+
if (!stock.inStock) {
112+
insufficientInventory.push(item.code);
113+
}
114+
}
115+
}
116+
}
117+
118+
if (mismatchedPrice.length > 0 || insufficientInventory.length > 0) {
119+
console.error({ insufficientInventory, mismatchedPrice });
120+
return {
121+
body: JSON.stringify({
122+
details:
123+
(insufficientInventory.length > 0
124+
? `Insufficient inventory for these items: ${insufficientInventory}. `
125+
: "") +
126+
(mismatchedPrice.length > 0
127+
? `Price does not match for these items: ${mismatchedPrice}.`
128+
: ""),
129+
ok: false,
130+
}),
131+
statusCode: 200,
132+
};
133+
} else {
134+
console.log("All checks have passed");
135+
return {
136+
body: JSON.stringify({
137+
details: "",
138+
ok: true,
139+
}),
140+
statusCode: 200,
141+
};
142+
}
143+
} catch (error) {
144+
console.error(error);
145+
return {
146+
body: JSON.stringify({
147+
details: "An internal error has occurred",
148+
ok: false,
149+
}),
150+
statusCode: 500,
151+
};
152+
}
153+
}
154+
155+
module.exports = {
156+
handler,
157+
};

0 commit comments

Comments
 (0)