# How To Implement a Referral System in Your Medusa Store

Referral systems are a powerful marketing tool that leverage existing customers to promote a product or service to their network. Businesses can tap into their loyal customer base by rewarding customers to refer others and expand their reach organically.

In this tutorial, you will explore how to implement a referral system in your Medusa e-commerce store. By implementing this feature, your customers will have the ability to refer other users and receive discounts on their future purchases. Additionally, the referred customers will also be eligible for a discount when they use a referral code during the sign-up process.

Here is a quick demo of what you're going to build in this tutorial:

%[https://youtu.be/zuur8gU-DX4] 

You can find the code for the tutorial in [this repository](https://github.com/ashutoshkrris/medusa-referral-system).

## What is Medusa?

Medusa is an open source, composable commerce platform designed for developers. With its flexible architecture, Medusa enables users to create custom commerce solutions that meet their specific needs. It provides modular commerce infrastructure that makes custom development processes easier for developers. Medusa leverages cutting-edge infrastructure technologies such as serverless and edge workers to ensure scalability, reliability, and performance.

![Medusa Architecture](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Docs/Diagrams/medusa-architecture-3_e385zk.jpg?_a=ATAPpAA0 align="center")

The highly extensible architecture enables developers to easily create custom features, such as automating customer group assignments and [integrating ChatGPT to automate writing product descriptions](https://medusajs.com/blog/chatgpt-medusa/). The concepts like [Subscribers](https://docs.medusajs.com/advanced/backend/subscribers/overview/) and [Events architecture](https://docs.medusajs.com/advanced/backend/events/architecture) in Medusa make it fairly simple to extend the store capabilities.

> Learn more about the architecture of Medusa in their [official documentation](https://docs.medusajs.com/development/fundamentals/architecture-overview).

## Prerequisites

Before you get started with the tutorial, you should have installed:

* [Node.js(V16 or later)](https://docs.medusajs.com/tutorial/set-up-your-development-environment#nodejs)
    
* [Git](https://docs.medusajs.com/tutorial/set-up-your-development-environment/#git)
    
* [Medusa CLI](https://docs.medusajs.com/tutorial/set-up-your-development-environment#medusa-cli)
    
* [Redis](https://docs.medusajs.com/tutorial/set-up-your-development-environment/#redis): Used by Medusa to handle the event queue.
    
* [PostgreSQL](https://docs.medusajs.com/development/backend/prepare-environment#postgresql)
    

## Setting up the Medusa Backend

The Medusa Backend serves as the central component responsible for the store's logic and data management. It exposes REST APIs that are utilized by the Medusa Admin and Storefront to perform operations like retrieving, creating, and modifying data.

In this section, you will go through setting up your Medusa backend. Follow these steps to create a new Medusa store named "my-medusa-store":

```bash
medusa new my-medusa-store
```

Open the `.env` file present in the `my-medusa-store` directory and add the database URL as below:

```bash
JWT_SECRET=something
COOKIE_SECRET=something

DATABASE_TYPE="postgres"
DATABASE_URL="postgres://<your-username>:<your-password>@localhost:5432/<your-database>"
REDIS_URL=redis://localhost:6379
```

> Note: Replace the `your-username`, `your-password` and `your-database` with your own values.

Next, open the `medusa-config.js` file and uncomment the `redis_url` in the `projectConfig` object:

```javascript
/** @type {import('@medusajs/medusa').ConfigModule["projectConfig"]} */
const projectConfig = {
  jwtSecret: process.env.JWT_SECRET,
  cookieSecret: process.env.COOKIE_SECRET,
  database_database: "./medusa-db.sql",
  database_type: DATABASE_TYPE,
  store_cors: STORE_CORS,
  admin_cors: ADMIN_CORS,
  // Uncomment the following lines to enable REDIS
  redis_url: REDIS_URL
}
```

Now, run the migrations and seed the test data present in the `data/seed.json` file to the PostgreSQL database:

```bash
cd my-medusa-store
medusa migrations run
medusa seed -f ./data/seed.json
```

Once the data is seeded, start the Medusa server by running this command:

```bash
medusa develop
```

These steps may take a few minutes to set up and run the server on port 9000. To test your server, you can either visit [**http://localhost:9000/store/products**](http://localhost:9000/store/products) in your browser or use the following command:

```bash
curl localhost:9000/store/products
```

If your server has been set up successfully, you will receive a response displaying a list of products and related details.

## Create a Referral Entity

The referral entity is an essential component of the referral system in Medusa. It represents a referral in the system and stores relevant information such as the referral code and the referrer customer. Let's explore why we need a referral entity and how to create it in the backend.

Start by creating a new `referral.ts` file in the `src/models` directory. This file will define the structure and behavior of your referral entity.

```typescript
import { BaseEntity } from "@medusajs/medusa"
import { Customer } from "@medusajs/medusa/dist/models"
import {
    Column,
    Entity,
    JoinColumn,
    OneToOne
} from "typeorm"

@Entity()
export class Referral extends BaseEntity {
    @Column({ type: "varchar" })
    referral_code: string | null

    @OneToOne(() => Customer)
    @JoinColumn({ name: "referrer_customer_id" })
    referrer_customer: Customer
}
```

In the above code, the `Referral` entity class extends the `BaseEntity` provided by Medusa to automatically inherits additional columns such as `id`, `created_at`, and `updated_at`. In addition to that, the entity includes the following attributes:

* `referral_code`: A string column that represents the referral code associated with the referral.
    
* `referrer_customer`: A one-to-one relationship with the `Customer` entity, representing the customer who referred others.
    

Medusa uses a specific format for entity IDs, which is `<PREFIX>_<RANDOM>`. For example, a referral might have an ID like `referral_01G35WVGY4D1JCA4TPGVXPGCQM`.

To generate an ID for the `Referral` entity that matches the IDs generated for Medusa's core entities, you need to add a `BeforeInsert` event handler. Inside this handler, you can use the `generateEntityId` utility function provided by Medusa to generate the ID. The `generateEntityId` function takes the current ID as the first parameter and the desired prefix as the second parameter.

```typescript
import { BaseEntity } from "@medusajs/medusa"
import { Customer } from "@medusajs/medusa/dist/models"
import { generateEntityId } from "@medusajs/medusa/dist/utils"
import {
    BeforeInsert,
    Column,
    Entity,
    JoinColumn,
    OneToOne
} from "typeorm"

@Entity()
export class Referral extends BaseEntity {
    @Column({ type: "varchar" })
    referral_code: string | null

    @OneToOne(() => Customer)
    @JoinColumn({ name: "referrer_customer_id" })
    referrer_customer: Customer

    @BeforeInsert()
    private beforeInsert(): void {
        this.id = generateEntityId(this.id, "referral")
    }
}
```

### Create a Migration

Since you created a new entity, you need to create a migration for your entity to update the database schema with the new table. To create a migration that makes changes to your Medusa schema, run the following command:

```bash
npx typeorm migration:create src/migrations/ReferralCreated
```

This will create the migration file in the specified path. The contents of the file looks like the below:

```typescript
import { MigrationInterface, QueryRunner } from "typeorm"

export class ReferralCreated1686235820432 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<void> {
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
    }
}
```

In the above code, the class implements the `MigrationInterface` from TypeORM, which requires the implementation of two methods: `up` and `down`. These methods define the actions to be performed when applying or reverting the migration, respectively.

Next, implement the `up` and `down` methods as below:

```typescript
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm"

export class ReferralCreated1686235820432 implements MigrationInterface {

    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.createTable(
            new Table({
                name: "referral",
                columns: [
                    {
                        name: "id",
                        type: "varchar",
                        isPrimary: true,
                    },
                    {
                        name: "referral_code",
                        type: "varchar",
                        isNullable: true,
                    },
                    {
                        name: "referrer_customer_id",
                        type: "varchar",
                    },
                    {
                        name: "created_at",
                        type: "timestamp",
                        default: "now()",
                    },
                    {
                        name: "updated_at",
                        type: "timestamp",
                        default: "now()",
                    },
                ],
            })
        );

        await queryRunner.createForeignKey(
            "referral",
            new TableForeignKey({
                columnNames: ["referrer_customer_id"],
                referencedColumnNames: ["id"],
                referencedTableName: "customer",
                onDelete: "CASCADE",
            })
        );
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.dropTable("referral");
    }

}
```

The `up` method performs two operations - create a new `referral` table with the defined columns and create the foreign key constraint on the `referrer_customer_id` column, referencing the `id` column of the `customer` table. The `down` method simply drops the `referral` table.

### Create a Repository

To conveniently access and modify data related to the Referral entity, you can create a TypeORM repository. Create a file named `referral.ts` in the `src/repositories` directory and add the following code to the file:

```typescript
import { dataSource } from "@medusajs/medusa/dist/loaders/database";
import { Referral } from "../models/referral";

export const ReferralRepository = dataSource
    .getRepository(Referral);

export default ReferralRepository;
```

The `ReferralRepository` is created using the `getRepository` method provided by the `dataSource` exported from the core package in Medusa.

### Run Migrations

Before you start using your entity, make sure to run the migrations that reflect the entity on your database schema. But before that, transpile the TypeScript files to JavaScript files by running the build command:

```bash
npm run build
```

Once the build process is complete, run the migration using the following command:

```bash
medusa migrations run
```

By creating the referral entity and repository, you have established the foundation for managing referral data in the backend. In the subsequent sections, you will build upon this by implementing services, APIs, and event subscribers to handle referral creation, validation, and updates.

## Create a Referral Service

The referral service class contains different methods for creating and saving new referrals to the database. To create a referral service, create a `referral.ts` file in the `src/services` folder and add the following code:

```typescript
import { TransactionBaseService } from "@medusajs/medusa"
import { EntityManager } from "typeorm"
import ReferralRepository from "../repositories/referral"

type InjectedDependencies = {
    manager: EntityManager
    referralRepository: typeof ReferralRepository
}

class ReferralService extends TransactionBaseService {
    protected readonly referralRepository_: typeof ReferralRepository

    constructor({
        referralRepository
    }: InjectedDependencies) {
        super(arguments[0])
        this.referralRepository_ = referralRepository
    }

}

export default ReferralService
```

The `ReferralService` class extends the `TransactionBaseService` class. This allows the referral service to inherit and utilize the transaction-related methods and functionalities provided by the base service.

The constructor of the `ReferralService` class accepts an object that contains the injected dependencies. It calls the `super` method with `arguments[0]` to pass the dependencies to the parent class constructor. Additionally, it assigns the `referralRepository` to the `referralRepository_` property.

Next, create a method to save new referral in the database:

```typescript
import { TransactionBaseService } from "@medusajs/medusa"
import { Referral } from "models/referral"
import { EntityManager } from "typeorm"
import ReferralRepository from "../repositories/referral"

// Existing code here

class ReferralService extends TransactionBaseService {
    // Constructor here

    async create(referral: CreateReferralInput): Promise<Referral> {
        return await this.atomicPhase_(async (manager) => {
            const referralRepository = manager.withRepository(
                this.referralRepository_
            )
            const created = referralRepository.create(referral);
            const result = await referralRepository.save(created);

            return result;
        })
    }

}

export default ReferralService
```

The asynchronous `create` method creates and saves a `Referral` entity. It follows an atomic and transactional approach to ensure data integrity.

Within the function, the `atomicPhase_` method is used to encapsulate the operations, ensuring that they are executed atomically. The method takes a callback function with a `manager` parameter, which represents the TypeORM `EntityManager` instance responsible for managing database transactions.

Inside the callback function, a `referralRepository` variable is created by associating the `this.referralRepository_` with the current transaction using the `manager.withRepository()` method. This ensures that the repository operations are performed within the transaction context.

The function then proceeds to create a new `Referral` entity by invoking `referralRepository.create(referral)`. This prepares the entity with the provided data.

Next, the `created` entity is passed to `referralRepository.save(created)` to persist in the database. This triggers the actual database operation to insert the entity. Finally, the function returns a promise that resolves to the result of the `referralRepository.save()` operation, which is the saved `Referral` object.

### Create a Custom Input Type for Referral

The `create` method takes a `referral` argument of `CreateReferralInput` type which hasn't been created yet. Create a `referral.ts` file in the `src/types` directory to store this custom input type:

```typescript
import { Customer } from "@medusajs/medusa/dist/models/customer"

export type CreateReferralInput = {
    referral_code?: string
    referrer_customer?: Customer
}
```

The `CreateReferralInput` type defines the structure of the input object when creating a referral. It includes two optional properties: `referral_code` (representing the referral code) and `referrer_customer` (representing the referrer customer associated with the referral).

The final updated referral service looks like the below:

```typescript
import { TransactionBaseService } from "@medusajs/medusa"
import { Referral } from "models/referral"
import { EntityManager } from "typeorm"
import { CreateReferralInput } from "types/referral"
import ReferralRepository from "../repositories/referral"

type InjectedDependencies = {
    manager: EntityManager
    referralRepository: typeof ReferralRepository
}

class ReferralService extends TransactionBaseService {
    protected readonly referralRepository_: typeof ReferralRepository

    constructor({
        referralRepository
    }: InjectedDependencies) {
        super(arguments[0])
        this.referralRepository_ = referralRepository
    }

    async create(referral: CreateReferralInput): Promise<Referral> {
        return await this.atomicPhase_(async (manager) => {
            const referralRepository = manager.withRepository(
                this.referralRepository_
            )
            const created = referralRepository.create(referral);
            const result = await referralRepository.save(created);

            return result;
        })
    }

}

export default ReferralService
```

## Create a Subscriber for Referral Code Creation

You can utilize the event-driven architecture of Medusa to implement the functionality of generating a new referral code for each customer upon registration. When a customer registers on the application, the `customer.created` [event](https://docs.medusajs.com/development/events/events-list) is triggered. This event serves as a trigger point for executing custom logic. In this case, you would create a subscriber that is responsible for handling this event and generating a new referral code for the customer.

> Learn how you can create a subscriber in Medusa in their [official documentation](https://docs.medusajs.com/development/events/create-subscriber).

Create a `newReferralCode.ts` file in the `src/subscribers` directory and add the following code:

```typescript
import { CustomerService, EventBusService } from "@medusajs/medusa";
import ReferralService from "../services/referral";

type InjectedProperties = {
    eventBusService: EventBusService
    referralService: ReferralService
    customerService: CustomerService
}

class NewReferralCodeSubscriber {
    private referralService: ReferralService
    private customerService: CustomerService

    constructor(properties: InjectedProperties) {
        this.referralService = properties.referralService;
        this.customerService = properties.customerService;
        properties.eventBusService.subscribe("customer.created", this.assignNewReferralCode);
    }

    assignNewReferralCode = async (customer) => {

        // Generate new referral code
        let newReferralCode = this.generateReferralCode(6);

        await this.referralService.create({
            referral_code: newReferralCode,
            referrer_customer: customer
        })

        await this.customerService.update(customer.id, {
            metadata: { "referral_code": newReferralCode }
        })
    }

    generateReferralCode = (length) => {
        const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        const codeLength = length || 6;
        let referralCode = '';

        while (referralCode.length < codeLength) {
            const randomIndex = Math.floor(Math.random() * characters.length);
            const randomCharacter = characters.charAt(randomIndex);
            referralCode += randomCharacter;
        }

        return referralCode;
    }
}

export default NewReferralCodeSubscriber;
```

The `NewReferralCodeSubscriber` relies on several injected services: `EventBusService` from Medusa, `ReferralService`, and `CustomerService`. Upon instantiation, the `NewReferralCodeSubscriber` class assigns the injected services to their private properties. It subscribes to the `customer.created` event using the `eventBusService.subscribe` method and provides the `assignNewReferralCode` method as the event handler.

The `assignNewReferralCode` method is an asynchronous function that receives the `customer` object as its parameter. It generates a new referral code by invoking the `generateReferralCode` method. The generated code is then used to create a new referral entry through the `referralService.create` method, which associates the referral code with the customer.

The `generateReferralCode` method is a utility function used to generate a random alphanumeric code. It takes an optional `length` parameter to determine the length of the referral code. It iteratively selects random characters from a predefined character set, concatenating them to form the referral code.

Additionally, the handler also updates the customer's metadata with the newly generated referral code using the `customerService.update` method.

## Setting up the Medusa Storefront

The next task is to let the users enter a referral code while registering on the application. To implement this, you'd need to set up the Medusa Storefront. In this tutorial, you will set up the [Next.js](https://docs.medusajs.com/starters/nextjs-medusa-starter) storefront offered by Medusa.

To begin, create a new Next.js project using the Medusa starter template by running the following command:

```bash
npx create-next-app -e https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront
```

After the project is created, navigate to the newly created directory and rename the template environment variable file to use environment variables in development:

```bash
cd my-medusa-storefront
mv .env.template .env.local
```

Ensure that the Medusa server is running, and then start the storefront by running the following command:

```bash
npm run dev
```

This command will start the development server for the Medusa storefront on `localhost:8000`, allowing you to test and interact with the application locally.

## Add Referral Code Field in Registration Form

Now that your storefront is set up successfully, it's time to add the referral code field to the registration form.

Open the `src/modules/account/components/register/index.tsx` file and add a `referral_code` property in the `RegisterCredentials`:

```typescript
interface RegisterCredentials extends FieldValues {
  ...
  referral_code?: string
}
```

Next, in the `Register` component, add the input field for the referral code after the password input field:

```typescript
<Input
  label="Have a referral code?"
  {...register("referral_code")}
  autoComplete="off"
  errors={errors}
/>
```

At this point, the register page should look like the below:

![Updated Register Page](https://cdn.hashnode.com/res/hashnode/image/upload/v1686325197211/6334a3d4-ba9b-4ac0-afeb-e579a271d9ba.png align="center")

The `customers.create` method doesn't accept the referral code by default. Thus, before passing the credentials to the method, you'd need to extract the referral code from it as below:

```typescript
const onSubmit = handleSubmit(async (credentials) => {
    const { referral_code, ...registerCredentials } = credentials

    await medusaClient.customers
      .create(registerCredentials)
      .then(() => {
        refetchCustomer()
        router.push("/account")
      })
      .catch(handleError)
  })
```

In the above code, the `credentials` object is destructured to extract the `referral_code` value and the remaining `registerCredentials`. Then, you pass the `registerCredentials` to the `create` method.

## Implement the Referral Code Validation API

When the user enters a referral code, you must check whether the referral code entered by the user is valid and associated with any customer. To do this, you'd create an API endpoint in the backend, and make a call from the frontend.

Open the `src/api/index.ts` file and add the following code:

```typescript
import { Router } from "express"

export default (rootDirectory: string): Router | Router[] => {

  const router = Router()

  // add your custom routes here

  router.get("/store/referral/:referralCode",
    async (req, res) => {
      try {
        const referralService = req.scope.resolve("referralService");

        const referralCode = req.params.referralCode;

        const referral = await referralService.getByCode(referralCode);

        if (referral) {
          res.json(referral);
        } else {
          res.status(404).json({ error: "Referral not found" })
        }
      } catch (error) {
        res.status(500).json({ error: "Internal server error" });
      }
    }
  )

  return router;
}
```

In the above code, you create a GET endpoint at `/store/referral/:referralCode` . This endpoint expects a referral code as a parameter in the URL.

The `referralService` is used to retrieve the referral object corresponding to the given referral code using the `getByCode` method. If a referral is found, it is returned as a JSON response using `res.json(referral)`. Otherwise, if no referral is found, a 404 status code with an error message is sent as the response.

In case of any errors during the execution of the callback or the retrieval process, a generic error message with a 500 status code is returned.

> Learn more about creating an endpoint in Medusa in their [official documentation](https://docs.medusajs.com/development/endpoints/create).

### CORS Configuration

Since you'll be using the storefront to make a request on the above-created endpoint, you need to pass your endpoints Cross-Origin Resource Origin (CORS) options using the `cors` package.

1. Begin by importing the necessary utility functions and types from Medusa's packages along with the `cors` library:
    
    ```typescript
    import { getConfigFile, parseCorsOrigins } from "medusa-core-utils";
    import { ConfigModule } from "@medusajs/medusa/dist/types/global";
    import cors from "cors";
    ```
    
2. In the exported function, retrieve the CORS configurations of your backend using the utility functions you imported:
    
    ```typescript
    export default (rootDirectory: string): Router | Router[] => {
      const { configModule } =
        getConfigFile<ConfigModule>(rootDirectory, "medusa-config")
      const { projectConfig } = configModule
    
      // ...
    }
    ```
    
3. Create an object to hold the CORS configurations. Since it's a storefront endpoint, pass the origins specified in the `store_cors` property of the project configuration:
    
    ```typescript
    const corsOptions = {
      origin: projectConfig.store_cors.split(","),
      credentials: true,
    };
    ```
    
4. For the route you added, create an OPTIONS request and add `cors` as middleware for the route, passing it the CORS options:
    
    ```typescript
    router.options("/store/referral/:referralCode", cors(corsOptions))
    router.get("/store/referral/:referralCode", cors(corsOptions),
      async (req, res) => {
        // ...
    });
    ```
    

### The `getByCode` Service Method

If you noticed above, you have used a `getByCode` method from the referral service. But you haven't defined it yet. Open the `src/services/referral.ts` file and add the following method to the `ReferralService` class:

```typescript
class ReferralService extends TransactionBaseService {
    ...

    async getByCode(referralCode: string): Promise<Referral> {
        return await this.atomicPhase_(async (manager) => {
            const referralRepository = manager.withRepository(
                this.referralRepository_
            )
            const referral = await referralRepository
                .createQueryBuilder("referral")
                .leftJoinAndSelect("referral.referrer_customer", "referrer_customer")
                .where("referral.referral_code = :referralCode", { referralCode })
                .getOne();

            return referral;
        })
    }
}

export default ReferralService
```

The above code defines an `async` function named `getByCode` that retrieves a referral based on a given referral code.

A query builder is utilized to construct a query for retrieving the referral. It performs a left join with the `referrer_customer` entity, allowing the referral's associated customer data to be included in the result.

The query filters the referrals based on the provided `referralCode` using the `where` clause. It retrieves only one referral using the `getOne` method, indicating that the referral code is expected to be unique.

Once the referral is retrieved, it is returned as the result of the `getByCode` function.

### Using the API To Validate the Referral Code

Now that you have created the API endpoint to validate the referral code, you'd need to use it in the storefront. Open the file and update the `onSubmit` function as below:

```typescript
const onSubmit = handleSubmit(async (credentials) => {
  let referrerCustomerId: string = ""
  const { referral_code, ...registerCredentials } = credentials
  if (referral_code) {
    referrerCustomerId = await getReferrerCustomerId(referral_code)
    if (!referrerCustomerId) {
      return
    }
  }

  await medusaClient.customers
    .create(registerCredentials)
    .then(() => {
      refetchCustomer()
      router.push("/account")
    })
    .catch(handleError)
})
```

The above code uses a `getReferrerCustomerId` function to get the referrer customer's ID. If no referrer's customer ID is found, the function returns early.

Next, create the `getReferrerCustomerId` function where you send a GET request on the newly created API endpoint:

```typescript
  const [referralError, setReferralError] = useState<string | undefined>(undefined)
  
  const getReferrerCustomerId = async (referralCode: string) => {
    try {
      const response = await fetch(
        `http://localhost:9000/store/referral/${referralCode}`
      )

      if (!response.ok) {
        setReferralError("The referral code is invalid. Please check.")
        return null
      }
      const referral = await response.json()

      return referral.referrer_customer.id
    } catch (error) {
      setReferralError(
        "An error occurred while validating the referral code. Please try again."
      )
      return null
    }
  }
```

In the above code, you first initialize a state variable for the referral error that may occur while handling the API request.

Next, the `getReferrerCustomerId` function is an asynchronous function that takes a `referralCode` parameter. Within the function, it attempts to make a GET request on the API endpoint with the referral code as a parameter.

If the response received from the server indicates an error (not a 2xx status code), the `setReferralError` function is called to update the `referralError` state with an appropriate error message. The function then returns `null` to indicate an invalid referral code.

If the response is successful, the function parses the response body as JSON using `response.json()`. It assumes that the response body contains the referral object. The function then returns the ID of the referrer's customer extracted from the parsed referral object.

If any error occurs during the HTTP request or JSON parsing, the `catch` block is executed. It sets the `referralError` state to an error message indicating that an error occurred while validating the referral code. The function returns `null` to indicate the validation failure.

You can use the `referralError` state variable in the `Register` component to display the referral-related errors as below:

```typescript
{referralError && (
  <div>
    <span className="text-rose-500 w-full text-small-regular">
      {referralError}
    </span>
  </div>
)}
```

### Display the Customer’s Referral Code

Ideally, a customer should be able to view its referral code and share it with others. But as of now, the customer cannot view it.

Open the `src/modules/account/components/overview/index.tsx` file in the storefront and add the following code:

```typescript
<div className="flex flex-col gap-y-4">
  <div className="flex items-center gap-x-2">
    <h3 className="text-large-semi">Referral Details</h3>
  </div>
  <p>
    Your referral code is{" "}
    <strong>{customer?.metadata["referral_code"] || "loading..."}</strong>.
   </p>
</div>
```

If you remember, in one of the previous sections, you had stored the `referral_code` in the customer's `metdata`. In the above code, you used the customer's `metadata` to get the value of `referral_code` key.

## Demo: Using Referal Code While Registration

%[https://youtu.be/eydh63jq6xQ] 

In the above demo, the process begins with a regular user signing up for an account without using a referral code. Once the registration is complete, the customer gains access to their profile where they can find their unique referral code and share it.

Following that, another user attempts to register for an account. Initially, this user enters an invalid referral code, resulting in an error message. However, upon entering a valid referral code, the registration process is successful, and the user can now view their own referral code within their account.

In the upcoming sections, you'll implement the discounting logic for the referrer and referred customers.

## Implementing Discounts for the Referrer and Referred Customers

Implementing discounts for the referrer and referred customers involves setting up a system where customers who refer others receive discounts, and the referred customers also receive their own discounts.

Here's a breakdown of the approach you're going to follow:

1. Frontend Implementation:
    
    * When a user enters a referral code during registration, the frontend will send a request to the `/store/referral/:referralCode` API endpoint to retrieve the referrer customer's ID.
        
    * After the account is created, the frontend will obtain the referred customer's ID.
        
2. Backend Implementation:
    
    * You will create a new API endpoint `/store/referral/discounts` that accepts the referrer and referred customer IDs as a POST request.
        
    * This endpoint will trigger two events internally: `referrer-discount.update` and `referred-discount.update`.
        
    * In the backend, you will create two subscribers for these events, which will add the referrer and referred customers to their respective customer groups: "Referrer Customers" and "Referred Customers".
        
3. Medusa Admin Configuration:
    
    * Using the Medusa Admin, you can configure the appropriate discounts for the referrer and referred customers. These discounts can be applied to their respective customer groups.
        
4. Tracking Referral Count:
    
    * You will maintain a `total_referrals` key in the customer's metadata. This key will represent the total number of customers who have successfully used a customer's referral code.
        
    * Whenever a customer uses the discount referral discount applicable to it, the `total_referrals` count will decrease by 1.
        
    * Similarly, when a customer signs up using a referral code, the `total_referrals` count will increase by 1.
        

### Frontend Implementation

To implement the functionality in the front end, as previously mentioned, the first step is to retrieve the IDs of the referred and referrer customers. Once obtained, you can proceed with making a POST request to the designated API endpoint. This section will guide you through the necessary steps to implement this process in the front end of your application.

Open the `src/modules/account/components/register/index.tsx` and update the `onSubmit` method as below:

```typescript
const onSubmit = handleSubmit(async (credentials) => {
    let referrerCustomerId: string = ""
    const { referral_code, ...registerCredentials } = credentials
    if (referral_code) {
      referrerCustomerId = await getReferrerCustomerId(referral_code)
      if (!referrerCustomerId) {
        return
      }
    }

    let referredCustomerId: string = ""

    await medusaClient.customers
      .create(registerCredentials)
      .then(({ customer }) => {
        referredCustomerId = customer.id
        refetchCustomer()
        router.push("/")
        updateDiscounts(referredCustomerId, referrerCustomerId)
      })
      .catch(handleError)
  })
```

In the above code, you declare a `referredCustomerId` variable and initialize it as an empty string. If the customer creation is successful, the `then()` block is executed, and the customer object is destructured from the response as `{ customer }`. The `referredCustomerId` is assigned the value of the newly created customer's ID.

Finally, you invoke the `updateDiscounts(referredCustomerId, referrerCustomerId)` function to initiate updating discounts for the referred and referrer customers, passing their respective IDs.

```typescript
const updateDiscounts = async (
    referredCustomerId: string,
    referrerCustomerId: string
  ) => {
    try {
      const response = await fetch(
        "http://localhost:9000/store/referral/discounts",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ referredCustomerId, referrerCustomerId }),
        }
      )

      if (response.ok) {
        const responseData = await response.json()
        console.log(responseData) // Handle the response data as needed
      } else {
        const errorData = await response.json()
        console.error(errorData) // Handle the error response
      }
    } catch (error) {
      console.error("An error occurred while updating discounts", error) // Handle any other errors
    }
  }
```

The above code defines an asynchronous function called `updateDiscounts`, which is responsible for making a POST request to update discounts for the referred and referrer customers.

The function uses the `fetch()` function to send a POST request to the `/store/referral/discounts` endpoint. The request includes the `referredCustomerId` and `referrerCustomerId` values in the request body as a JSON payload.

### Backend Implementation

Let's create the `/store/referral/discounts` API endpoint in the backend. Open the `src/api/index.ts` file and add the following code:

```typescript
const bodyParser = require("body-parser")

export default (rootDirectory: string): Router | Router[] => {

  // ...

  const router = Router()
  router.use(bodyParser.json());

  // add your custom routes here

  // ...

  router.options("/store/referral/discounts", cors(corsOptions))
  router.post("/store/referral/discounts", cors(corsOptions),
    async (req, res) => {
      try {
        const eventBusService = req.scope.resolve("eventBusService");
        const customerService = req.scope.resolve("customerService");

        const { referredCustomerId, referrerCustomerId } = req.body;

        if (referrerCustomerId) {
          eventBusService.emit("referred-discount.update", { referredCustomerId });
          eventBusService.emit("referrer-discount.update", { referrerCustomerId });
          const referrerCustomer = await customerService.retrieve(referrerCustomerId)

          const totalReferrals = referrerCustomer.metadata
            && typeof referrerCustomer.metadata.total_referrals === "number"
            ? referrerCustomer.metadata.total_referrals
            : 0;

          await customerService.update(referrerCustomerId, {
            metadata: {
              "total_referrals": totalReferrals + 1
            }
          })
        }
        res.json({ success: true });
      } catch (error) {
        console.log(error);

        res.status(500).json({ error: "Internal server error" });
      }
    }
  )

  return router;
}
```

The above code defines an API endpoint that handles updating discounts for referred and referrer customers. When a POST request is made to the `/store/referral/discounts` endpoint, the code retrieves the required services and data from the request.

Since you're creating a POST endpoint, you'd need to import the `body-parser` library and use it.

If a referrer customer ID is provided, the code emits events to update the discounts for both the referred and referrer customers. It also retrieves the referrer customer's metadata, increments the total referral count, and updates the metadata accordingly. This condition check is important because you don't want to emit the events if the customer hasn't been referred by other customers.

Finally, a success response is sent back to the client. If any errors occur, an error response is returned. Overall, this code facilitates the process of updating discounts and tracking the number of referrals for customers.

## Subscribers Implementation

Now, you need two subscribers that listen to the events emitted by the API endpoint. The subscribers add the customers to their respective customer groups.

#### The "Referrer Customers" Customer Group

Create a `referrerCustomerGroupAssignment.ts` file in the `src/subscribers` directory and add the following code:

```typescript
import { CustomerGroupService, EventBusService } from "@medusajs/medusa"


type InjectedProperties = {
    eventBusService: EventBusService
    customerGroupService: CustomerGroupService
}


class ReferrerCustomerGroupAssignmentSubscriber {
    private customerGroupService: CustomerGroupService
    private customerGroupName: string

    constructor(properties: InjectedProperties) {
        this.customerGroupService = properties.customerGroupService;
        this.customerGroupName = "Referrer Customers";
        properties.eventBusService.subscribe("referrer-discount.update", this.handleGroupAssignment);
    }

    handleGroupAssignment = async ({ referrerCustomerId }) => {

        let customerGroup;

        // Check if "Referrer Customers" customer group exists
        let customerGroupList = await this.customerGroupService.list({ name: this.customerGroupName }, { take: 1 });

        // If it doesn't exist, create it
        if (!customerGroupList.length) {
            customerGroup = await this.customerGroupService.create({ name: this.customerGroupName });
        } else {
            customerGroup = customerGroupList[0];
        }

        customerGroup = await this.customerGroupService.retrieve(customerGroup.id, { relations: ["customers"] })

        const isCustomerInGroup = customerGroup.customers.some(
            (customer) => customer.id === referrerCustomerId
        );

        if (isCustomerInGroup) return;

        // Add customer to "Referrer Customers" customer group
        await this.customerGroupService.addCustomers(customerGroup.id, [referrerCustomerId]);
    }
}

export default ReferrerCustomerGroupAssignmentSubscriber;
```

The above code defines a class called `ReferrerCustomerGroupAssignmentSubscriber`. This class is responsible for assigning customers to the "Referrer Customers" customer group when a referral discount is updated.

The class has a constructor that receives `InjectedProperties` as input, which includes the `eventBusService` and `customerGroupService` from the Medusa library. In the constructor, the `eventBusService` is used to subscribe to the event `referrer-discount.update` and associate it with the `handleGroupAssignment` method.

The `handleGroupAssignment` method is triggered when the `referrer-discount.update` event is emitted. It retrieves the `referrerCustomerId` from the event payload.

The code then checks if the "Referrer Customers" customer group already exists by listing customer groups with the specified name. If it doesn't exist, a new customer group is created using the `customerGroupService`.

Next, the code retrieves the customer group and its associated customers. It checks if the `referrerCustomerId` is already present in the customer group. If the customer is already in the group, the method returns early. If the customer is not in the group, the `referrerCustomerId` is added to the "Referrer Customers" customer group using the `customerGroupService`.

#### The "Referred Customers" Customer Group

Create a `referredCustomerGroupAssignment.ts` file in the `src/subscribers` directory and add the following code:

```typescript
import { CustomerGroupService, EventBusService } from "@medusajs/medusa"


type InjectedProperties = {
    eventBusService: EventBusService
    customerGroupService: CustomerGroupService
}


class ReferredCustomerGroupAssignmentSubscriber {
    private customerGroupService: CustomerGroupService
    private customerGroupName: string

    constructor(properties: InjectedProperties) {
        this.customerGroupService = properties.customerGroupService;
        this.customerGroupName = "Referred Customers";
        properties.eventBusService.subscribe("referred-discount.update", this.handleGroupAssignment);
    }

    handleGroupAssignment = async ({ referredCustomerId }) => {

        let customerGroup;

        // Check if "Referred Customers" customer group exists
        let customerGroupList = await this.customerGroupService.list({ name: this.customerGroupName }, { take: 1 });

        // If it doesn't exist, create it
        if (!customerGroupList.length) {
            customerGroup = await this.customerGroupService.create({ name: this.customerGroupName });
        } else {
            customerGroup = customerGroupList[0];
        }

        // Add customer to "Referred Customers" customer group
        await this.customerGroupService.addCustomers(customerGroup.id, [referredCustomerId]);
    }
}

export default ReferredCustomerGroupAssignmentSubscriber;
```

The above code defines a class called `ReferredCustomerGroupAssignmentSubscriber`. This class is responsible for assigning customers to the "Referred Customers" customer group when a referred customer discount is updated.

The `InjectedProperties` type specifies the injected dependencies for the class, which include the `eventBusService` and `customerGroupService`.

The class has a constructor that receives the `InjectedProperties` as input. It assigns the `customerGroupService` and sets the `customerGroupName` to "Referred Customers". It then subscribes to the `referred-discount.update` event using the `eventBusService` and associates it with the `handleGroupAssignment` method.

The `handleGroupAssignment` method is triggered when the `referred-discount.update` event is emitted. It retrieves the `referredCustomerId` from the event payload.

The code then checks if the "Referred Customers" customer group already exists by listing customer groups with the specified name. If it doesn't exist, a new customer group is created using the `customerGroupService`.

Finally, the `referredCustomerId` is added to the "Referred Customers" customer group using the `customerGroupService`.

## Setting up the Medusa Admin

You'll require the Medusa Admin to set up the coupon code discounts for the customer groups. In this section, you'll set up the Medusa Admin Dashboard.

To install the Medusa Admin Dashboard in your Medusa backend directory, run the following command:

```typescript
npm install @medusajs/admin
```

Next, open the `medusa-config.js` file in your Medusa backend directory. Inside the `plugins` array, add the following configuration for the Admin plugin:

```typescript
const plugins = [
  // ...
  {
    resolve: "@medusajs/admin",
    /** @type {import('@medusajs/admin').PluginOptions} */
    options: {
      // ...
    },
  },
]
```

> Check out the different [options](https://docs.medusajs.com/admin/quickstart#step-2-add-admin-to-medusa-configurations) accepted by the admin plugin.

Test the admin dashboard by running the following command in the directory of the Medusa backend:

```bash
medusa develop
```

The Admin Dashboard will be available on the URL [`localhost:9000/app`](http://localhost:9000/app) . Since you've already seeded the data, you can use the email `admin@medusa-test.com` and password `supersecret` to log in.

## Creating Discounts using Admin Dashboard

In this section, you'll create two customer groups - Referrer Customers and Referred Customers, using the Admin Dashboard. Next, you'll create a discount coupon code for each of the customer groups.

Follow the video to see how you can do it:

%[https://youtu.be/A6aC5s_oBo4] 

In the video, the following two referral discount coupon codes are created:

1. NEW10 - 10% discount applicable for the new customers who used a valid referral code during registration
    
2. REFER20 - 20% discount applicable for customers whose referral code was used during registration
    

## Tracking Referral Discounts

Since you maintain a `total_referrals` count in the customer's metadata, you would need to update it once the user applies the "REFER20" or "NEW10" coupon code and places an order.

To update the `total_referrals` count and manage customer groups based on coupon codes, you can implement the following logic:

1. For referred customers:
    
    * If they apply the "NEW10" coupon code, remove the customer from the "Referred Customers" customer group.
        
2. For referrer customers:
    
    * If they apply the "REFER20" coupon code, reduce the `total_referrals` count by 1.
        
    * If the `total_referrals` count was previously 1, set it to 0, and remove the customer from the "Referrer Customers" customer group.
        

When a customer places an order, the `order.placed` event is emitted. You can create a subscriber that listens to the event and implements the above logic.

Create a `discount.ts` file in the `src/subscribers` directory and add the following code:

```typescript
import { CustomerGroupService, CustomerService, EventBusService, OrderService } from "@medusajs/medusa"


type InjectedProperties = {
    eventBusService: EventBusService
    customerGroupService: CustomerGroupService
    customerService: CustomerService
    orderService: OrderService
}


class DiscountSubscriber {
    private customerGroupService: CustomerGroupService
    private customerService: CustomerService
    private orderService: OrderService
    private referredCustomerGroupName: string
    private referrerCustomerGroupName: string

    constructor(properties: InjectedProperties) {
        this.customerGroupService = properties.customerGroupService;
        this.customerService = properties.customerService;
        this.orderService = properties.orderService;
        this.referredCustomerGroupName = "Referred Customers";
        this.referrerCustomerGroupName = "Referrer Customers";

        properties.eventBusService.subscribe("order.placed", this.handleDiscounts);
    }

    handleDiscounts = async ({ id }) => {

        // Retrieve the order by id
        const order = await this.orderService.retrieve(id, { relations: ["discounts"] });

        // Get the code used by the customer
        const discountCodeUsed = order.discounts && order.discounts[0].code;

        // Get the customer
        const customer = await this.customerService.retrieve(order.customer_id, { relations: ["groups"] });

        if (discountCodeUsed === "NEW10") {
            const customerGroup = customer.groups.find(group => group.name === this.referredCustomerGroupName);
            await this.customerGroupService.removeCustomer(customerGroup.id, [customer.id]);
        } else if (discountCodeUsed === "REFER20") {
            const totalReferrals = customer.metadata.total_referrals as number;

            if (totalReferrals == 1) {
                await this.customerService.update(customer.id, {
                    metadata: { "total_referrals": 0 }
                })
                const customerGroup = customer.groups.find(group => group.name === this.referrerCustomerGroupName);
                await this.customerGroupService.removeCustomer(customerGroup.id, [customer.id]);
            } else if (totalReferrals > 1) {
                await this.customerService.update(customer.id, {
                    metadata: { "total_referrals": totalReferrals - 1 }
                })
            }
        }
    }
}

export default DiscountSubscriber;
```

The above code defines a `DiscountSubscriber` class that listens for the `order.placed` event through the `eventBusService` and associates the `handleDiscounts` method.

In the `handleDiscounts` method, the code retrieves the order details and extracts the discount code used by the customer. By utilizing the `customerService`, it retrieves the customer information associated with the order. The code then proceeds to evaluate the discount code used.

If the discount code is identified as "NEW10", it means the customer applied this code. Consequently, the customer is removed from the "Referred Customers" group using the `customerGroupService`. This step ensures that referred customers no longer remain in the group once they have used the "NEW10" discount.

On the other hand, if the discount code is recognized as "REFER20", the code accesses the customer's metadata to obtain the `total_referrals` count. If the count is equal to 1, it implies that the customer had only one referral. In this scenario, the code updates the customer's metadata to set the `total_referrals` count to 0. Additionally, the customer is removed from the "Referrer Customers" group.

However, if the `total_referrals` count is greater than 1, the code simply decreases the count by 1 in the customer's metadata, representing the utilization of one referral while maintaining the customer's presence in the "Referrer Customers" group.

You'd also want the customers to see their total number of referrals. Open the `src/modules/account/components/overview/index.tsx` file in the storefront and add the following code:

```typescript
<div className="flex flex-col gap-y-4">
  <div className="flex items-center gap-x-2">
    <h3 className="text-large-semi">Referral Details</h3>
  </div>
  <p>
    Your referral code is{" "}
    <strong>{customer?.metadata["referral_code"] || "loading..."}</strong>
    . You have referred{" "}
    <strong>{customer?.metadata["total_referrals"] || 0}</strong>{" "}
    customers till now.
  </p>
</div>
```

## Demo: Using Referral Discounts

%[https://youtu.be/Mu8wnt5U8DE] 

## Wrapping Up

In this tutorial, you have covered the step-by-step process of implementing a referral system in Medusa. With a functional referral system in place, you can leverage the power of customer referrals to expand your customer base and drive business growth.

This implementation was possible because of Medusa’s composable architecture. You can learn more about Medusa through its [documentation](https://docs.medusajs.com/). Here are some documents that you can start with:

* [Create an entity in Medusa](https://docs.medusajs.com/development/entities/create)
    
* [Create a service in Medusa](https://docs.medusajs.com/development/services/create-service)
    
* [Create an API endpoint in Medusa](https://docs.medusajs.com/development/endpoints/create)
    
* [Create a subscriber in Medusa](https://docs.medusajs.com/development/events/create-subscriber)
    

> Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord.
