# Updating the amount of discount on each order

## What are we building?

We want to run a loyalty campaign for our customers. It will give them increasing discounts with each order. Here's how it will work:

* When a customer checks out, they can enter the discount code `ORDER2EARN` to get a 10% discount.
* If the customer doesn't cancel their subscription, they can get a 20% discount on their second order.
* If the customer continues their subscription for one more cycle, they can get a 30% discount on their third order.
* The campaign only applies to the first three orders, so there won't be any more discounts after that.

## How to change promotion for each order?

With regular discount codes, we can only apply one promotion to a discount code. This means that every order will receive the same discount until the code expires.

To update the discount for each order. we are going to use webhooks. We will first track when customer makes a new order. Then we will update the promotion applied to customer's subscription using GraphQL API calls.

## Preparation

### Creating a project access token

To update subscription promotions using the GraphQL API, we will need a project access token with ***Write*** access type. You can create this token on the Firmhouse Portal by going to the ***Settings > Integrations*** page.

### Setting up the discounts and discount codes

For our campaign we need three discounts with 10%, 20%, 30% discount percentages. You can create them on ***Discounts*** page on Firmhouse Portal. To ensure each customer can use each discount only once, we will set the ***Limit discount per customer*** setting to ***1***.

<div align="center" data-full-width="true"><figure><img src="/files/n14ejYIZoH5ymAt6VaZk" alt=""><figcaption><p>First Discount 10%</p></figcaption></figure> <figure><img src="/files/phFeVPlDNQQxQGNxWHv8" alt=""><figcaption><p>Second Discount 20%</p></figcaption></figure> <figure><img src="/files/2IiKBSkaOCKjv8hP8dBJ" alt=""><figcaption><p>Third discount 30%</p></figcaption></figure></div>

Then we need to create a discount code, that activates the first discount. You can create that on ***Discount Codes*** page on the Firmhouse Portal

<figure><img src="/files/bbpdZPpmUEuBaF9ITSVx" alt="" width="251"><figcaption><p>ORDER2EARN discount code</p></figcaption></figure>

### Webhook Deployment

We need to create an HTTP endpoint that acts as a webhook for Firmhouse to call. While you have the freedom to choose any programming language or tool like AWS Lambda or Azure Functions, for this tutorial, we will use [Pipedream](https://pipedream.com/) with JavaScript to deploy our webhook handler.

## Webhook Handler

When the webhook handler receives the event, it needs to check if the subscription has the discount code we created. Then, we need to decide which promotion to add based on the latest promotion applied.

To understand what data we can get from the webhook call, let's start by writing the template for the webhook. In this case, we will use the ***Order Confirmed*** event.

To fetch the details of the subscription later using a GraphQL API call, we will need the subscription's token.

Although it's not necessary, we can also include the subscription's discount codes. This will allow us to exit early in the webhook handler if the campaign discount code is not used.

#### &#x20;<a href="#webhook-template" id="webhook-template"></a>

<pre class="language-liquid" data-title="Order Confirmed template"><code class="lang-liquid">


{
  "event": "order_confirmed",
  "subscription": {
    "<a data-footnote-ref href="#user-content-fn-1">token</a>": "{{subscription.token}}",
    "<a data-footnote-ref href="#user-content-fn-2">codes</a>":{% if codes.size > 0 %} ["{{codes_string}}"] {% else %} [] {% endif %}

  }
}
</code></pre>

{% hint style="info" %}
Why we use ***Order Confirmed*** event?

After an order is confirmed, Firmhouse will update the limits of promotions applied to the subscription. If the limits of promotions are exceeded, the promotion will be disabled. So, when we receive the event on our webhook handler, we can check if there is a valid promotion applied to the subscription. If we cannot find any, then we can apply the next promotion in our campaign.
{% endhint %}

To start, we need to extract the payload from the webhook call. This step is the only part that varies depending on the libraries/frameworks you are using. Firmhouse will send the payload using a POST request, and the data will be in the request body. You can refer to the instructions specific to your framework/library to learn how to access the request body of an HTTP call.

{% tabs %}
{% tab title="Pipedream JS" %}

```javascript
export default defineComponent({
  props: {
    firmhouseProjectAccessToken: {
      type: "string",
      label: "Firmhouse project access token with write",
      secret: true,
    },
  },
  async run({ steps, $ }) {
    const PROJECT_ACCESS_TOKEN = this.firmhouseProjectAccessToken
    const body = steps.trigger.event.body;
    const payload = body.subscription
       
    // the rest of our code will go here
    // ...
  },
});
```

{% endtab %}

{% tab title="express.js" %}

```javascript
const app = express();
app.use(express.json());

// You need to set the project access token from PROJECT_ACCESS_TOKEN env variable
const PROJECT_ACCESS_TOKEN = process.env.PROJECT_ACCESS_TOKEN

app.post('/webhook', function (req, res) {
　　const body = req.body;
　　const payload = body.subscription
    // the rest of our code will go here
    // ...
});

app.listen(3000, function (err) {
    if (err) console.log(err);
    console.log("Server listening on PORT", PORT);
});
```

{% endtab %}

{% tab title="Serverless Framework Node.js" %}
{% code title="serverless.yml" %}

```yaml
functions:
  webhook:
    handler: handler.webhook
    environment:
      PROJECT_ACCESS_TOKEN: ${env:PROJECT_ACCESS_TOKEN}
    events:
      - http: 
          path: webhook
          method: post
          async: true
          cors: true
```

{% endcode %}

{% code title="handler.js" %}

```javascript
module.exports.webhook = function (event, context, callback) {
  const PROJECT_ACCESS_TOKEN = process.env.PROJECT_ACCESS_TOKEN
  const body = JSON.parse(event.body);
  const payload = body.subscription
  // The rest of our code will go here
  // ... 
 
  callback(null, {statusCode: 200, body: ''});
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Note that, the `Content-Type` header for the post request will be `text/plain.`If the http server you use depends on Content-Type header to parse the json body, you may need to parse the body as a JSON with `JSON.parse(body)` first.
{% endhint %}

Let's specify the discount code we're searching for and verify if it exists in the payload before making any API calls, in order to quickly exit from the handler for subscriptions that do not use the discount code.

```javascript
const DISCOUNT_CODE = 'ORDER2EARN'
if (!payload.codes.includes(DISCOUNT_CODE)) return;
```

{% hint style="info" %}
You can make the discount code configurable with an environment variable or component props in Pipedream. You can also set up multiple discount codes for various campaigns. We're keeping it simple here to avoid making this tutorial more complicated.
{% endhint %}

We also need to decide which promotions to apply and in what order. We can use an array of promotion IDs for that. To find the IDs, go to the Discounts page and click the Edit button next to the discount you created for this campaign. You will find the promotion ID in the address bar. The format will be like this:

```
/projects/<PROJECT_ID>/order_discount_promotions/<PROMOTION_ID>/edit
```

After obtaining the IDs of each promotion, let's create an array with them. Make sure to write them in the order they will be applied, so that we know which promotion to apply next, later on.

```javascript
const PROMOTION_IDS = ["116728", "116729", "116730"];
```

{% hint style="info" %}
Note that the first promotion id in this array will be directly applied with the discount code. We are just keeping it here to be able to check if the promotion is still valid and applied.
{% endhint %}

We have to get all the promotion details that were applied to the subscription using the GraphQL API. To do this, we will use the subscription token value from the webhook payload and pass it along with the project access token we created to the following function.

<pre class="language-javascript"><code class="lang-javascript"><strong>async function getSubscription(projectAccessToken, subscriptionToken) {
</strong>  const headers = new Headers({
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
  });
  const graphql = JSON.stringify({
    query: `
      query($token: ID!) {
        getSubscription(token: $token) {
          id
          appliedPromotions {
            active
            promotion {
                id
                percentDiscount
                title
                activated
            }
            discountCode {
                code
                expired
                metadata
            }
          }
        }
     }
  `,
    variables: {
      token: subscriptionToken,
    },
  });

  const requestOptions = {
    method: "POST",
    headers,
    body: graphql,
  };

  const response = await fetch(
    "https://portal.firmhouse.com/graphql",
    requestOptions
  );
  const body = await response.json();
  return body.data.getSubscription;
}
</code></pre>

Now that we have a method to retrieve subscription details, we can do so using the subscription token in our payload.

```javascript
const subscription = await getSubscription(
    PROJECT_ACCESS_TOKEN,
    payload.token
  ); 
```

If there is an active campaign promotion, we don't need to add any additional promotions, so we can simply exit in that case. This won't occur if you set the Limit discount per customer setting to 1 when creating the promotion, as it will be deactivated after being used once. However, if it can be used multiple times, we need to make sure that we are not attempting to apply two promotions simultaneously.

```javascript
// Creating a set of promotion ids for efficient lookup
const campaignPromotionIdsSet = new Set(PROMOTION_IDS);
const { appliedPromotions } = subscription;

// Check if one of the promotions are still active
const activePromotion = appliedPromotions.find(
  (ap) => ap.active && campaignPromotionIdsSet.has(ap.promotion.id)
);

if (activePromotion) {
  console.log(`Subscription ${subscription.id} has an active promotion ${activePromotion.promotion.id}, not adding another one`);
  return;
}
```

Currently, we have confirmed that the discount code is being used and there are no active promotions on the subscription. Now, we can check if there are any additional promotions that we can include. If we have no more promotions to add, we can exit the handler at this point.

<pre class="language-javascript"><code class="lang-javascript">// Make a set out of the promotion ids previously applied to subscription for efficient lookup
const appliedPromotionIds = new Set(
  appliedPromotions.map((ap) => ap.promotion.id)
);

// Find the promotion that is not applied yet
// We are iterating the promotion ids in order
// So the first one we find, is the next one we should apply
const nextPromotionId = PROMOTION_IDS.find(
  (id) => !appliedPromotionIds.has(id)
);

// If there is no more promotions that we can apply, we can stop here
if (!nextPromotionId) {
<strong>  console.log(
</strong>    `There is no promotion left to add for the subscription`
  );
  return;
}
</code></pre>

We now have the next promotion id to apply to the subscription. So we need to call the GraphQL API to apply it.

We will create a function that executes the ***applyPromotionToSubscription*** mutation on the GraphQL API. This function will require the subscription id, promotion id, and project access token as input.

```javascript
async function applyPromotionToSubscription(
  subscriptionId,
  promotionId,
  projectAccessToken
) {
  const headers = new Headers({
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
  });
  const graphql = JSON.stringify({
    query: `
      mutation($input: ApplyPromotionToSubscriptionInput!) {
        applyPromotionToSubscription(input: $input) {
          errors {
            message
            attribute
            path
          }
        }
     }
    `,
    variables: {
      input: {
        subscriptionId,
        promotionId,
      },
    },
  });

  const requestOptions = {
    method: "POST",
    headers,
    body: graphql,
  };

  const response = await fetch(
    "https://portal.firmhouse.com/graphql",
    requestOptions
  );
  const body = await response.json();
  const { errors } = body.data.applyPromotionToSubscription;
  if (errors) {
    console.log(errors);
  }
}
```

Finally, we can use the `applyPromotionToSubscription` method to apply the next promotion to customer's subscription.

```javascript
console.log(`Adding promotion ${nextPromotionId} to subscription ${subscription.id}`);
await applyPromotionToSubscription(
  subscription.id,
  nextPromotionId,
  PROJECT_ACCESS_TOKEN
);
```

Before deploying, it is recommended to secure the endpoint. Firmhouse currently supports basic authentication, which allows you to set a username and password for the webhook. These credentials will be sent in the Authorization header, in the following format.

```
Authorization: Basic <"username:password" as base64 encoded string>
```

We can declare a function to check if the Authorization header is correct.

```javascript
async function checkAuthorization(authorizationHeader, validUsername, validPassword) {
  if (validUsername && validPassword) {
    const validAuth = new Buffer(`${validUsername}:${validPassword}`).toString(
      "base64"
    );
    if (authorizationHeader !== `Basic ${validAuth}`) {
      return false;
    }
  }
  return true;
}
```

Let's use the method to check if the authorization is valid. Remember to modify how you access the authorization header based on the library/framework you are using.

```javascript
const username = this.webhookUsername
const password = this.webhookPassword
    
if(!checkAuthorization(steps.trigger.event.headers.authorization, username, password)) {
    console.log('Not authorized')
    return;
}
```

To get a full picture of the handler here is the complete version with Authentication.

<details>

<summary>Handler</summary>

```javascript
async function getSubscription(projectAccessToken, subscriptionToken) {
  const headers = new Headers({
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
  });
  const graphql = JSON.stringify({
    query: `
        query($token: ID!) {
          getSubscription(token: $token) {
            id
            appliedPromotions {
              active
              promotion {
                  id
                  percentDiscount
                  title
                  activated
              }
              discountCode {
                  code
                  expired
                  metadata
              }
            }
          }
       }
    `,
    variables: {
      token: subscriptionToken,
    },
  });

  const requestOptions = {
    method: "POST",
    headers,
    body: graphql,
  };

  const response = await fetch(
    "https://portal.firmhouse.com/graphql",
    requestOptions
  );
  const body = await response.json();
  return body.data.getSubscription;
}
async function applyPromotionToSubscription(
  subscriptionId,
  promotionId,
  projectAccessToken
) {
  const headers = new Headers({
    "Content-Type": "application/json",
    "X-Project-Access-Token": projectAccessToken,
  });
  const graphql = JSON.stringify({
    query: `
        mutation($input: ApplyPromotionToSubscriptionInput!) {
          applyPromotionToSubscription(input: $input) {
            errors {
              message
              attribute
              path
            }
          }
       }
      `,
    variables: {
      input: {
        subscriptionId,
        promotionId,
      },
    },
  });

  const requestOptions = {
    method: "POST",
    headers,
    body: graphql,
  };

  const response = await fetch(
    "https://portal.firmhouse.com/graphql",
    requestOptions
  );
  const body = await response.json();
  const { errors } = body.data.applyPromotionToSubscription;
  if (errors) {
    console.log(errors);
  }
}
async function checkAuthorization(authorizationHeader, validUsername, validPassword) {
    if (validUsername && validPassword) {
      const validAuth = new Buffer(`${validUsername}:${validPassword}`).toString(
        "base64"
      );
      if (authorizationHeader !== `Basic ${validAuth}`) {
        return false;
      }
    }
    return true;
  }

export default defineComponent({
  props: {
    firmhouseProjectAccessToken: {
      type: "string",
      label: "Firmhouse project access token with write",
      secret: true,
    },
    webhookUsername: {
      type: "string",
      label: "The username for the webhook",
    },
    webhookPassword: {
      type: "string",
      label: "The password for the webhook",
      secret: true,
    },
  },
  async run({ steps, $ }) {
    const username = this.webhookUsername;
    const password = this.webhookPassword;
    
    if(!checkAuthorization(steps.trigger.event.headers.authorization, username, password)) {
        console.log('Not authorized')
        return;
    }
    
    const PROJECT_ACCESS_TOKEN = this.firmhouseProjectAccessToken;
    const body = steps.trigger.event.body;
    const payload = body.subscription;

    const DISCOUNT_CODE = "ORDER2EARN";
    if (!payload.codes.includes(DISCOUNT_CODE)) return;
    const PROMOTION_IDS = ["116728", "116729", "116730"];

    const subscription = await getSubscription(
      PROJECT_ACCESS_TOKEN,
      payload.token
    );

    const campaignPromotionIdsSet = new Set(PROMOTION_IDS);
    const { appliedPromotions } = subscription;

    // Check if one of the promotions are still active
    const activePromotion = appliedPromotions.find(
      (ap) => ap.active && campaignPromotionIdsSet.has(ap.promotion.id)
    );

    if (activePromotion) {
      console.log(
        `Subscription ${subscription.id} has an active promotion ${activePromotion.promotion.id}, not adding another one`
      );
      return;
    }

    // Make a set out of the promotion ids previously applied to subscription for efficient lookup
    const appliedPromotionIds = new Set(
      appliedPromotions.map((ap) => ap.promotion.id)
    );

    // Find the promotion that is not applied yet
    // We are iterating the promotion ids in order
    // So the first one we find, is the next one we should apply
    const nextPromotionId = PROMOTION_IDS.find(
      (id) => !appliedPromotionIds.has(id)
    );

    // If there is no more promotions that we can apply, we can stop here
    if (!nextPromotionId) {
      console.log(`There is no promotion left to add for the subscription`);
      return;
    }

    console.log(
      `Adding promotion ${nextPromotionId} to subscription ${subscription.id}`
    );
    await applyPromotionToSubscription(
      subscription.id,
      nextPromotionId,
      PROJECT_ACCESS_TOKEN
    );
  },
});

```

</details>

## Deployment and Testing

We have our handler ready, now we need to deploy it so that Firmhouse can call it as a webhook.

1. First create a new workflow in Pipedream.\\

   <div align="center"><figure><img src="/files/5igjaZPMepjJgOQbJw3p" alt=""><figcaption><p>Create new workflow</p></figcaption></figure> <figure><img src="/files/H8AZ0qzrg37h4IaZPDzm" alt=""><figcaption><p>Workflow settings</p></figcaption></figure></div>
2. Create a new HTTP/ Webhook Request trigger and then copy the URL. We will use it while creating the webhook on Firmhouse Portal.

<div data-full-width="false"><figure><img src="/files/3amBzD5achs6c9rVRJkw" alt=""><figcaption><p>Webhook Request Trigger</p></figcaption></figure> <figure><img src="/files/Qbskjbj3VKi1KgOmV8Wc" alt=""><figcaption><p>Trigger settings</p></figcaption></figure> <figure><img src="/files/NDElXFiMQV6CZ2cWJIM9" alt=""><figcaption><p>The webhook URL</p></figcaption></figure></div>

3. Create new step to add our handler then copy our handler code to there. Then you can click on refresh fields button to display the configuration fields. Then enter the project access token, webhook username and webhook password to those fields.\\

   <div><figure><img src="/files/F4gWqEg0orhh7em9UFp1" alt=""><figcaption><p>Run custom code step</p></figcaption></figure> <figure><img src="/files/oMy2Atydb0BtmYPZKJXy" alt=""><figcaption><p>Configuration fields</p></figcaption></figure></div>
4. Now you can create the webhook on Firmhouse portal. In Firmhouse navigate to ***Apps*** in the sidebar. Find the ***Webhooks*** app and click ***Configure***. Then you can fill the details as shown in the picture. You can copy the webhook payload template we created [here](#webhook-template). After you fill the details click on ***Save.*** \\

   <figure><img src="/files/62IUft7J29hALF1we9nc" alt=""><figcaption><p>Webhook configuration</p></figcaption></figure>
5. Now go ahead and create a new subscription with the discount code ***ORDER2EARN.*** After you complete the payment, you will see the event on Pipedream. You can select the event then click on ***Continue*** and then ***Test*** to test the handler with that event. You can check the logs to see if worked correctly. If it did, you can check that on ***Customers*** page on Firmhouse as well. Select the customer you just created, on ***Applied discounts*** section you should see that second order discount is applied.\\

   <figure><img src="/files/QArUAwbXLKiZIJru9m2W" alt="" width="375"><figcaption><p>Events show up on pipedream</p></figcaption></figure>

   <figure><img src="/files/mWrmQKwIkItdcKymGOmf" alt=""><figcaption><p>Second discount is applied successfully!</p></figcaption></figure>
6. After you make sure that everything is working correctly, you can click ***Deploy*** on Pipedream to let it process events as they arrive.

   <figure><img src="/files/9LaTvD32Jf1Ck9jjC3kC" alt="" width="375"><figcaption><p>Deploy on Pipedream</p></figcaption></figure>

[^1]: Subscription token

[^2]: Discount codes applied to payload


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://developer.firmhouse.com/guides/updating-the-amount-of-discount-on-each-order.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
