Table of contents
- What is Medusa?
- Prerequisites
- Setting up the Medusa Backend
- Create a Referral Entity
- Create a Referral Service
- Create a Subscriber for Referral Code Creation
- Setting up the Medusa Storefront
- Add Referral Code Field in Registration Form
- Implement the Referral Code Validation API
- Demo: Using Referal Code While Registration
- Implementing Discounts for the Referrer and Referred Customers
- Subscribers Implementation
- Setting up the Medusa Admin
- Creating Discounts using Admin Dashboard
- Tracking Referral Discounts
- Demo: Using Referral Discounts
- Wrapping Up
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:
You can find the code for the tutorial in this repository.
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.
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. The concepts like Subscribers and Events architecture in Medusa make it fairly simple to extend the store capabilities.
Learn more about the architecture of Medusa in their official documentation.
Prerequisites
Before you get started with the tutorial, you should have installed:
Redis: Used by Medusa to handle the event queue.
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":
medusa new my-medusa-store
Open the .env
file present in the my-medusa-store
directory and add the database URL as below:
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
andyour-database
with your own values.
Next, open the medusa-config.js
file and uncomment the redis_url
in the projectConfig
object:
/** @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:
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:
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 localhost:9000/store/products in your browser or use the following command:
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.
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 theCustomer
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.
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:
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:
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:
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:
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:
npm run build
Once the build process is complete, run the migration using the following command:
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:
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:
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:
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:
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 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.
Create a newReferralCode.ts
file in the src/subscribers
directory and add the following code:
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 storefront offered by Medusa.
To begin, create a new Next.js project using the Medusa starter template by running the following command:
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:
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:
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
:
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:
<Input
label="Have a referral code?"
{...register("referral_code")}
autoComplete="off"
errors={errors}
/>
At this point, the register page should look like the below:
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:
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:
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.
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.
Begin by importing the necessary utility functions and types from Medusa's packages along with the
cors
library:import { getConfigFile, parseCorsOrigins } from "medusa-core-utils"; import { ConfigModule } from "@medusajs/medusa/dist/types/global"; import cors from "cors";
In the exported function, retrieve the CORS configurations of your backend using the utility functions you imported:
export default (rootDirectory: string): Router | Router[] => { const { configModule } = getConfigFile<ConfigModule>(rootDirectory, "medusa-config") const { projectConfig } = configModule // ... }
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:const corsOptions = { origin: projectConfig.store_cors.split(","), credentials: true, };
For the route you added, create an OPTIONS request and add
cors
as middleware for the route, passing it the CORS options: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:
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:
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:
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:
{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:
<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
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:
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.
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
andreferred-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".
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.
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:
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.
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:
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:
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:
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:
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:
const plugins = [
// ...
{
resolve: "@medusajs/admin",
/** @type {import('@medusajs/admin').PluginOptions} */
options: {
// ...
},
},
]
Check out the different options accepted by the admin plugin.
Test the admin dashboard by running the following command in the directory of the Medusa backend:
medusa develop
The Admin Dashboard will be available on the URL 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:
In the video, the following two referral discount coupon codes are created:
NEW10 - 10% discount applicable for the new customers who used a valid referral code during registration
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:
For referred customers:
- If they apply the "NEW10" coupon code, remove the customer from the "Referred Customers" customer group.
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:
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:
<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
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. Here are some documents that you can start with:
Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord.