# Digital Products Recipe Example

In this guide, you'll learn how to support digital products in Medusa.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. Medusa provides all features related to products and managing them, and the Medusa Framework allows you to extend those features and implement your custom use case.

You can extend Medusa's product features to support selling, storing, and fulfilling digital products. In this guide, you'll customize Medusa to add the following features:

1. Support digital products with multiple media items.
2. Manage digital products from the admin dashboard.
3. Handle and fulfill digital product orders.
4. Allow customers to download their digital product purchases from the storefront.
5. All other commerce features that Medusa provides.

This guide provides an example of an approach to implement digital products. You're free to choose a different approach using the Medusa Framework.

- [Digital Products Example Repository](https://github.com/medusajs/examples/tree/main/digital-product): Find the full code for this recipe example in this repository.
- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1721654620/OpenApi/Digital_Products_Postman_vjr3jg.yml): Imported this OpenApi Specs file into tools like Postman.

***

## Step 1: Install a Medusa Application

### Prerequisites

- [Node.js v20+](https://nodejs.org/en/download)
- [Git CLI tool](https://git-scm.com/downloads)
- [PostgreSQL](https://www.postgresql.org/download/)

Start by installing the Medusa application on your machine with the following command:

```bash npx2yarnExec
npx create-medusa-app@latest
```

You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter), choose Yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name.

The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/learn/introduction/architecture).

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.

Check out the [troubleshooting guides](https://docs.medusajs.com/troubleshooting/create-medusa-app-errors) for help.

***

## Step 2: Create the Digital Product Module

Medusa creates commerce features in modules. For example, product features and data models are created in the Product Module.

You also create custom commerce data models and features in custom modules. They're integrated into the Medusa application similar to Medusa's modules without side effects.

So, you'll create a digital product module that holds the data models related to a digital product and allows you to manage them.

Create the directory `src/modules/digital-product`.

### Create Data Models

Create the file `src/modules/digital-product/models/digital-product.ts` with the following content:

```ts title="src/modules/digital-product/models/digital-product.ts"
import { model } from "@medusajs/framework/utils"
import DigitalProductMedia from "./digital-product-media"
import DigitalProductOrder from "./digital-product-order"

const DigitalProduct = model.define("digital_product", {
  id: model.id().primaryKey(),
  name: model.text(),
  medias: model.hasMany(() => DigitalProductMedia, {
    mappedBy: "digitalProduct",
  }),
  orders: model.manyToMany(() => DigitalProductOrder, {
    mappedBy: "products",
  }),
})
.cascades({
  delete: ["medias"],
})

export default DigitalProduct
```

This creates a `DigitalProduct` data model. It has many medias and orders, which you’ll create next.

Create the file `src/modules/digital-product/models/digital-product-media.ts` with the following content:

```ts title="src/modules/digital-product/models/digital-product-media.ts" highlights={dpmModelHighlights}
import { model } from "@medusajs/framework/utils"
import { MediaType } from "../types"
import DigitalProduct from "./digital-product"

const DigitalProductMedia = model.define("digital_product_media", {
  id: model.id().primaryKey(),
  type: model.enum(MediaType),
  fileId: model.text(),
  mimeType: model.text(),
  digitalProduct: model.belongsTo(() => DigitalProduct, {
    mappedBy: "medias",
  }),
})

export default DigitalProductMedia
```

This creates a `DigitalProductMedia` data model, which represents a media file that belongs to the digital product. The `fileId` property holds the ID of the uploaded file as returned by the File Module, which is explained in later sections.

Notice that the above data model uses an enum from a `types` file. So, create the file `src/modules/digital-product/types/index.ts` with the following content:

```ts title="src/modules/digital-product/types/index.ts"
export enum MediaType {
  MAIN = "main",
  PREVIEW = "preview"
}
```

This enum indicates that a digital product media can either be used to preview the digital product, or is the main file available on purchase.

Next, create the file `src/modules/digital-product/models/digital-product-order.ts` with the following content:

```ts title="src/modules/digital-product/models/digital-product-order.ts"
import { model } from "@medusajs/framework/utils"
import { OrderStatus } from "../types"
import DigitalProduct from "./digital-product"

const DigitalProductOrder = model.define("digital_product_order", {
  id: model.id().primaryKey(),
  status: model.enum(OrderStatus),
  products: model.manyToMany(() => DigitalProduct, {
    mappedBy: "orders",
    pivotTable: "digitalproduct_digitalproductorders",
  }),
})

export default DigitalProductOrder
```

This creates a `DigitalProductOrder` data model, which represents an order of digital products.

This data model also uses an enum from the `types` file. So, add the following to the `src/modules/digital-product/types/index.ts` file:

```ts title="src/modules/digital-product/types/index.ts"
export enum OrderStatus {
  PENDING = "pending",
  SENT = "sent"
}
```

### Create Main Module Service

Next, create the main service of the module at `src/modules/digital-product/service.ts` with the following content:

```ts title="src/modules/digital-product/service.ts"
import { MedusaService } from "@medusajs/framework/utils"
import DigitalProduct from "./models/digital-product"
import DigitalProductOrder from "./models/digital-product-order"
import DigitalProductMedia from "./models/digital-product-media"

class DigitalProductModuleService extends MedusaService({
  DigitalProduct,
  DigitalProductMedia,
  DigitalProductOrder,
}) {

}

export default DigitalProductModuleService
```

The service extends the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory), which provides basic data-management features.

### Create Module Definition

After that, create the module definition at `src/modules/digital-product/index.ts` with the following content:

```ts title="src/modules/digital-product/index.ts"
import DigitalProductModuleService from "./service"
import { Module } from "@medusajs/framework/utils"

export const DIGITAL_PRODUCT_MODULE = "digitalProductModuleService"

export default Module(DIGITAL_PRODUCT_MODULE, {
  service: DigitalProductModuleService,
})
```

### Add Module to Medusa Configuration

Finally, add the module to the list of modules in `medusa-config.ts`:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    {
      resolve: "./src/modules/digital-product",
    },
  ],
})
```

### Further Reads

- [How to Create a Module](https://docs.medusajs.com/learn/fundamentals/modules)
- [How to Create a Data Model](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model)

***

## Step 3: Define Links

Modules are isolated in Medusa, making them reusable, replaceable, and integrable in your application without side effects.

So, you can't have relations between data models in modules. Instead, you define a link between them.

Links are relations between data models of different modules that maintain the isolation between the modules.

In this step, you’ll define links between your module’s data models and data models from Medusa’s Commerce Modules:

1. Link between the `DigitalProduct` model and the Product Module's `ProductVariant` model.
2. Link between the `DigitalProductOrder` model and the Order Module's `Order` model.

Start by creating the file `src/links/digital-product-variant.ts` with the following content:

```ts title="src/links/digital-product-variant.ts"
import DigitalProductModule from "../modules/digital-product"
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: DigitalProductModule.linkable.digitalProduct,
    deleteCascade: true,
  },
  ProductModule.linkable.productVariant
)

```

This defines a link between `DigitalProduct` and the Product Module’s `ProductVariant`. This allows product variants that customers purchase to be digital products.

`deleteCascades` is enabled on the `digitalProduct` so that when a product variant is deleted, its linked digital product is also deleted.

Next, create the file `src/links/digital-product-order.ts` with the following content:

```ts title="src/links/digital-product-order.ts"
import DigitalProductModule from "../modules/digital-product"
import OrderModule from "@medusajs/medusa/order"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
  {
    linkable: DigitalProductModule.linkable.digitalProductOrder,
    deleteCascade: true,
  },
  OrderModule.linkable.order
)

```

This defines a link between `DigitalProductOrder` and the Order Module’s `Order`. This keeps track of orders that include purchases of digital products.

`deleteCascades` is enabled on the `digitalProductOrder` so that when a Medusa order is deleted, its linked digital product order is also deleted.

### Further Read

- [How to Define Module Links](https://docs.medusajs.com/learn/fundamentals/module-links)

***

## Step 4: Run Migrations and Sync Links

To create tables for the digital product data models in the database, start by generating the migrations for the Digital Product Module with the following command:

```bash npx2yarn
npx medusa db:generate digitalProductModuleService
```

This generates a migration in the `src/modules/digital-product/migrations` directory.

Then, reflect the migrations and links in the database with the following command:

```bash npx2yarn
npx medusa db:migrate
```

***

## Step 5: List Digital Products Admin API Route

To expose custom commerce features to frontend applications, such as the Medusa Admin dashboard or a storefront, you expose an endpoint by creating an API route.

In this step, you’ll create the admin API route to list digital products.

Create the file `src/api/admin/digital-products/route.ts` with the following content:

```ts title="src/api/admin/digital-products/route.ts"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { 
    data: digitalProducts,
    metadata: { count, take, skip } = {},
  } = await query.graph({
    entity: "digital_product",
    ...req.queryConfig,
  })

  res.json({
    digital_products: digitalProducts,
    count,
    limit: take,
    offset: skip,
  })
}
```

This adds a `GET` API route at `/admin/digital-products`.

In the route handler, you use Query to retrieve the list of digital products and their relations. The route handler also supports pagination.

Then, create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { validateAndTransformQuery } from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"

const GetDigitalProductsSchema = createFindParams()

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/digital-products",
      method: "GET",
      middlewares: [
        validateAndTransformQuery(
          GetDigitalProductsSchema,
          {
            defaults: [
              "id",
              "name",
              "created_at",
              "updated_at",
              "deleted_at",
              "medias.*",
              "product_variant.*",
            ],
            isList: true,
          }
        ),
      ],
    },
  ],
})
```

### Further Read

- [How to Create a Middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares)

### Test API Route

To test out the API route, start the Medusa application:

```bash npm2yarn
npm run dev
```

Then, obtain a JWT token as an admin user with the following request:

```bash
curl -X POST 'http://localhost:9000/auth/user/emailpass' \
-H 'Content-Type: application/json' \
--data-raw '{
    "email": "admin@medusajs.com",
    "password": "supersecret"
}'
```

Finally, send the following request to retrieve the list of digital products:

```bash
curl -L 'http://localhost:9000/admin/digital-products' \
-H 'Authorization: Bearer {token}'
```

Make sure to replace `{token}` with the JWT token you retrieved.

### Further Reads

- [How to Create an API Route](https://docs.medusajs.com/learn/fundamentals/api-routes)
- [Learn more about Query](https://docs.medusajs.com/learn/fundamentals/module-links/query)

***

## Step 6: Create Digital Product Workflow

To implement and expose a feature that manipulates data, you create a workflow that uses services to implement the functionality, then create an API route that executes that workflow.

In this step, you’ll create a workflow that creates a digital product. You’ll use this workflow in an API route in the next section.

This workflow has the following steps:

```mermaid
graph TD
  createProductsWorkflow["createProductsWorkflow (Medusa)"] --> createDigitalProductStep
  createDigitalProductStep --> createDigitalProductMediasStep
  createDigitalProductMediasStep --> createRemoteLinkStep["createRemoteLinkStep (Medusa)"]
```

1. `createProductsWorkflow`: Create the Medusa product that the digital product is associated with its variant. Medusa provides this workflow through the `@medusajs/medusa/core-flows` package, which you can use as a step.
2. `createDigitalProductStep`: Create the digital product.
3. `createDigitalProductMediasStep`: Create the medias associated with the digital product.
4. `createRemoteLinkStep`: Create the link between the digital product and the product variant. Medusa provides this step through the `@medusajs/medusa/core-flows` package.

You’ll implement the second and third steps.

### createDigitalProductStep (Second Step)

Create the file `src/workflows/create-digital-product/steps/create-digital-product.ts` with the following content:

```ts title="src/workflows/create-digital-product/steps/create-digital-product.ts" highlights={createDpHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import DigitalProductModuleService from "../../../modules/digital-product/service"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"

export type CreateDigitalProductStepInput = {
  name: string
}

const createDigitalProductStep = createStep(
  "create-digital-product-step",
  async (data: CreateDigitalProductStepInput, { container }) => {
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    const digitalProduct = await digitalProductModuleService
      .createDigitalProducts(data)
    
    return new StepResponse({
      digital_product: digitalProduct,
    }, {
      digital_product: digitalProduct,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)
    
    await digitalProductModuleService.deleteDigitalProducts(
      data.digital_product.id
    )
  }
)

export default createDigitalProductStep
```

This creates the `createDigitalProductStep`. In this step, you create a digital product.

In the compensation function, which is executed if an error occurs in the workflow, you delete the digital products.

### createDigitalProductMediasStep (Third Step)

Create the file `src/workflows/create-digital-product/steps/create-digital-product-medias.ts` with the following content:

```ts title="src/workflows/create-digital-product/steps/create-digital-product-medias.ts" highlights={createDigitalProductMediaHighlights} collapsibleLines="1-8" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import DigitalProductModuleService from "../../../modules/digital-product/service"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"
import { MediaType } from "../../../modules/digital-product/types"

export type CreateDigitalProductMediaInput = {
  type: MediaType
  fileId: string
  mimeType: string
  digital_product_id: string
}

type CreateDigitalProductMediasStepInput = {
  medias: CreateDigitalProductMediaInput[]
}

const createDigitalProductMediasStep = createStep(
  "create-digital-product-medias",
  async ({ 
    medias,
  }: CreateDigitalProductMediasStepInput, { container }) => {
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    const digitalProductMedias = await digitalProductModuleService
      .createDigitalProductMedias(medias)

    return new StepResponse({
      digital_product_medias: digitalProductMedias,
    }, {
      digital_product_medias: digitalProductMedias,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)
    
    await digitalProductModuleService.deleteDigitalProductMedias(
      data.digital_product_medias.map((media) => media.id)
    )
  }
)

export default createDigitalProductMediasStep
```

This creates the `createDigitalProductMediasStep`. In this step, you create medias of the digital product.

In the compensation function, you delete the digital product medias.

### Create createDigitalProductWorkflow

Finally, create the file `src/workflows/create-digital-product/index.ts` with the following content:

```ts title="src/workflows/create-digital-product/index.ts" highlights={createDpWorkflowHighlights} collapsibleLines="1-23" expandMoreLabel="Show Imports"
import { 
  createWorkflow,
  transform,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  CreateProductWorkflowInputDTO,
} from "@medusajs/framework/types"
import { 
  createProductsWorkflow,
  createRemoteLinkStep,
} from "@medusajs/medusa/core-flows"
import { 
  Modules,
} from "@medusajs/framework/utils"
import createDigitalProductStep, { 
  CreateDigitalProductStepInput,
} from "./steps/create-digital-product"
import createDigitalProductMediasStep, { 
  CreateDigitalProductMediaInput,
} from "./steps/create-digital-product-medias"
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"

type CreateDigitalProductWorkflowInput = {
  digital_product: CreateDigitalProductStepInput & {
    medias: Omit<CreateDigitalProductMediaInput, "digital_product_id">[]
  }
  product: CreateProductWorkflowInputDTO
}

const createDigitalProductWorkflow = createWorkflow(
  "create-digital-product",
  (input: CreateDigitalProductWorkflowInput) => {
    const product = createProductsWorkflow.runAsStep({
      input: {
        products: [input.product],
      },
    })

    const digitalProductData = transform(
      input,
      (data) => ({
        name: data.digital_product.name,
      })
    )

    const { digital_product } = createDigitalProductStep(
      digitalProductData
    )

    const { digital_product_medias } = createDigitalProductMediasStep(
      transform({
        digital_product,
        input,
      },
      (data) => ({
        medias: data.input.digital_product.medias.map((media) => ({
          ...media,
          digital_product_id: data.digital_product.id,
        })),
      })
      )
    )

    createRemoteLinkStep([{
      [DIGITAL_PRODUCT_MODULE]: {
        digital_product_id: digital_product.id,
      },
      [Modules.PRODUCT]: {
        product_variant_id: product[0].variants[0].id,
      },
    }])

    const returnData = transform(
      {
        digital_product,
        digital_product_medias,
      },
      (data) => ({
        digital_product: {
          ...data.digital_product,
          medias: data.digital_product_medias,
        },
      })
    )

    return new WorkflowResponse(returnData)
  }
)

export default createDigitalProductWorkflow
```

This creates the `createDigitalProductWorkflow`. The workflow accepts as a parameter the digital product and the Medusa product to create.

In the workflow, you run the following steps:

1. `createProductsWorkflow` as a step to create a Medusa product.
2. `createDigitalProductStep` to create the digital product.
3. `createDigitalProductMediasStep` to create the digital product’s medias.
4. `createRemoteLinkStep` to link the digital product to the product variant.

You’ll test out the workflow in the next section.

### Further Reads

- [How to Create a Workflow](https://docs.medusajs.com/learn/fundamentals/workflows)
- [What is the Compensation Function](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function)
- [Learn more about Link functions](https://docs.medusajs.com/learn/fundamentals/module-links/link)

***

## Step 7: Create Digital Product API Route

In this step, you’ll add the API route to create a digital product using the `createDigitalProductWorkflow`.

In the file `src/api/admin/digital-products/route.ts` add a new route handler:

As of [Medusa v2.13.0](https://github.com/medusajs/medusa/releases/tag/v2.13.0), Zod should be imported from `@medusajs/framework/zod`.

```ts title="src/api/admin/digital-products/route.ts"
// other imports...
import { z } from "@medusajs/framework/zod"
import createDigitalProductWorkflow from "../../../workflows/create-digital-product"
import { CreateDigitalProductMediaInput } from "../../../workflows/create-digital-product/steps/create-digital-product-medias"
import { createDigitalProductsSchema } from "../../validation-schemas"

// ...

type CreateRequestBody = z.infer<
  typeof createDigitalProductsSchema
>

export const POST = async (
  req: AuthenticatedMedusaRequest<CreateRequestBody>,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
  const { data: [shippingProfile] } = await query.graph({
    entity: "shipping_profile",
    fields: ["id"],
  })

  const { result } = await createDigitalProductWorkflow(
    req.scope
  ).run({
    input: {
      digital_product: {
        name: req.validatedBody.name,
        medias: req.validatedBody.medias.map((media) => ({
          fileId: media.file_id,
          mimeType: media.mime_type,
          type: media.type,
        })) as Omit<CreateDigitalProductMediaInput, "digital_product_id">[],
      },
      product: {
        ...req.validatedBody.product,
        shipping_profile_id: shippingProfile.id,
      },
    },
  })

  res.json({
    digital_product: result.digital_product,
  })
}
```

This adds a `POST` API route at `/admin/digital-products`. In the route handler, you first retrieve a shipping profile to associate it with the product, which is required.

Then, you execute the `createDigitalProductWorkflow` created in the previous step, passing data from the request body as input, along with the retrieved shipping profile.

The route handler imports a validation schema from a `validation-schema` file. So, create the file `src/api/validation-schemas.ts` with the following content:

```ts title="src/api/validation-schemas.ts"
import { 
  AdminCreateProduct,
} from "@medusajs/medusa/api/admin/products/validators"
import { z } from "@medusajs/framework/zod"
import { MediaType } from "../modules/digital-product/types"

export const createDigitalProductsSchema = z.object({
  name: z.string(),
  medias: z.array(z.object({
    type: z.nativeEnum(MediaType),
    file_id: z.string(),
    mime_type: z.string(),
  })),
  product: AdminCreateProduct(),
})
```

This defines the expected request body schema.

Finally, add to the file `src/api/middlewares.ts` the following content:

```ts title="src/api/middlewares.ts"
import { 
  defineMiddlewares,
  validateAndTransformBody,
} from "@medusajs/framework/http"
import { createDigitalProductsSchema } from "./validation-schemas"

export default defineMiddlewares({
  routes: [
    {
      matcher: "/admin/digital-products",
      method: "POST",
      middlewares: [
        validateAndTransformBody(createDigitalProductsSchema),
      ],
    },
  ],
})
```

This adds a validation middleware to ensure that the body of `POST` requests sent to `/admin/digital-products` match the `createDigitalProductsSchema`.

### Further Read

- [How to Create a Middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares)

***

## Step 8: Upload Digital Product Media API Route

To upload the digital product media files, use Medusa’s File Module.

Your Medusa application uses the local file module provider by default, which uploads files to a local directory. However, you can use other file module providers, such as the [S3 module provider](https://docs.medusajs.com/infrastructure-modules/file/s3).

In this step, you’ll create an API route for uploading preview and main digital product media files.

Before creating the API route, install the [multer express middleware](https://expressjs.com/en/resources/middleware/multer.html) to support file uploads:

```bash npm2yarn
npm install multer
npm install --save-dev @types/multer
```

Then, create the file `src/api/admin/digital-products/upload/[type]/route.ts` with the following content:

```ts title="src/api/admin/digital-products/upload/[type]/route.ts" highlights={uploadHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import {
  AuthenticatedMedusaRequest,
  MedusaResponse,
} from "@medusajs/framework/http"
import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError } from "@medusajs/framework/utils"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const access = req.params.type === "main" ? "private" : "public"
  const input = req.files as Express.Multer.File[]

  if (!input?.length) {
    throw new MedusaError(
      MedusaError.Types.INVALID_DATA,
      "No files were uploaded"
    )
  }

  const { result } = await uploadFilesWorkflow(req.scope).run({
    input: {
      files: input?.map((f) => ({
        filename: f.originalname,
        mimeType: f.mimetype,
        content: f.buffer.toString("base64"),
        access,
      })),
    },
  })

  res.status(200).json({ files: result })
}

```

This adds a `POST` API route at `/admin/digital-products/upload/[type]` where `[type]` is either `preview` or `main`.

In the route handler, you use `uploadFilesWorkflow` from Medusa's core workflows to upload the file. If the file type is `main`, it’s uploaded with private access, as only customers who purchased it can download it. Otherwise, it’s uploaded with `public` access.

Next, add to the file `src/api/middlewares.ts` the `multer` middleware on this API route:

```ts title="src/api/middlewares.ts"
// other imports...
import multer from "multer"

const upload = multer({ storage: multer.memoryStorage() })

export default defineMiddlewares({
  routes: [
    // ...
    {
      matcher: "/admin/digital-products/upload**",
      method: "POST",
      middlewares: [
        upload.array("files"),
      ],
    },
  ],
})
```

You’ll test out this API route in the next step as you use these API routes in the admin customizations.

***

## Step 9: Add Digital Products UI Route in Admin

The Medusa Admin is customizable, allowing you to inject widgets into existing pages or add UI routes to create new pages.

In this step, you’ll add a UI route to the Medusa Admin that displays a list of digital products.

Before you create the UI route, create the file `src/admin/types/index.ts` that holds the following types:

```ts title="src/admin/types/index.ts"
import { ProductVariantDTO } from "@medusajs/framework/types"

export enum MediaType {
  MAIN = "main",
  PREVIEW = "preview"
}

export type DigitalProductMedia = {
  id: string
  type: MediaType
  fileId: string
  mimeType: string
  digitalProducts?: DigitalProduct
}

export type DigitalProduct = {
  id: string
  name: string
  medias?: DigitalProductMedia[]
  product_variant?:ProductVariantDTO
}

```

These types will be used by the UI route.

Next, create the file `src/admin/routes/digital-products/page.tsx` with the following content:

```tsx title="src/admin/routes/digital-products/page.tsx" highlights={digitalProductPageHighlights} collapsibleLines="1-7" expandMoreLabel="Show Imports"
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { PhotoSolid } from "@medusajs/icons"
import { Container, Heading, Table } from "@medusajs/ui"
import { useState } from "react"
import { Link } from "react-router-dom"
import { DigitalProduct } from "../../types"

const DigitalProductsPage = () => {
  const [digitalProducts, setDigitalProducts] = useState<
    DigitalProduct[]
  >([])
  // TODO fetch digital products...

  return (
    <Container>
      <div className="flex justify-between items-center mb-4">
        <Heading level="h2">Digital Products</Heading>
        {/* TODO add create button */}
      </div>
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Name</Table.HeaderCell>
            <Table.HeaderCell>Action</Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {digitalProducts.map((digitalProduct) => (
            <Table.Row key={digitalProduct.id}>
              <Table.Cell>
                {digitalProduct.name}
              </Table.Cell>
              <Table.Cell>
                <Link to={`/products/${digitalProduct.product_variant?.product_id}`}>
                  View Product
                </Link>
              </Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
      {/* TODO add pagination component */}
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Digital Products",
  icon: PhotoSolid,
})

export default DigitalProductsPage

```

This creates a UI route that's displayed at the `/digital-products` path in the Medusa Admin. The UI route also adds a sidebar item with the label “Digital Products" pointing to the UI route.

In the React component of the UI route, you just display the table of digital products.

Next, replace the first `TODO` with the following:

```tsx title="src/admin/routes/digital-products/page.tsx" highlights={paginationHighlights}
// other imports...
import { useMemo } from "react"

const DigitalProductsPage = () => {
  // ...
    
  const [currentPage, setCurrentPage] = useState(0)
  const pageLimit = 20
  const [count, setCount] = useState(0)
  const pagesCount = useMemo(() => {
    return count / pageLimit
  }, [count])
  const canNextPage = useMemo(
    () => currentPage < pagesCount - 1, 
    [currentPage, pagesCount]
  )
  const canPreviousPage = useMemo(
    () => currentPage > 0, 
    [currentPage]
  )

  const nextPage = () => {
    if (canNextPage) {
      setCurrentPage((prev) => prev + 1)
    }
  }

  const previousPage = () => {
    if (canPreviousPage) {
      setCurrentPage((prev) => prev - 1)
    }
  }
  
  // TODO fetch digital products
    
  // ...
}
```

This defines the following pagination variables:

1. `currentPage`: The number of the current page.
2. `pageLimit`: The number of digital products to show per page.
3. `count`: The total count of digital products.
4. `pagesCount`: A memoized variable that holds the number of pages based on `count` and `pageLimit`.
5. `canNextPage`: A memoized variable that indicates whether there’s a next page based on whether the current page is less than `pagesCount - 1`.
6. `canPreviousPage`: A memoized variable that indicates whether there’s a previous pages based on whether the current page is greater than `0`.
7. `nextPage`: A function that increments the `currentPage`.
8. `previousPage`: A function that decrements the `currentPage`.

Then, replace the new `TODO fetch digital products` with the following:

```tsx title="src/admin/routes/digital-products/page.tsx" highlights={fetchDigitalProductsHighlights}
// other imports
import { useEffect } from "react"

const DigitalProductsPage = () => {
  // ...
    
  const fetchProducts = () => {
    const query = new URLSearchParams({
      limit: `${pageLimit}`,
      offset: `${pageLimit * currentPage}`,
    })
    
    fetch(`/admin/digital-products?${query.toString()}`, {
      credentials: "include",
    })
    .then((res) => res.json())
    .then(({ 
      digital_products: data, 
      count,
    }) => {
      setDigitalProducts(data)
      setCount(count)
    })
  }

  useEffect(() => {
    fetchProducts()
  }, [currentPage])
    
  // ...
}
```

This defines a `fetchProducts` function that fetches the digital products using the API route you created in step 4. You also call that function within a `useEffect` callback which is executed whenever the `currentPage` changes.

Finally, replace the `TODO add pagination component` in the return statement with `Table.Pagination` component:

```tsx title="src/admin/routes/digital-products/page.tsx"
return (
    <Container>
      {/* ... */}
      <Table.Pagination
        count={count}
        pageSize={pageLimit}
        pageIndex={currentPage}
        pageCount={pagesCount}
        canPreviousPage={canPreviousPage}
        canNextPage={canNextPage}
        previousPage={previousPage}
        nextPage={nextPage}
      />
    </Container>
  )
```

The `Table.Pagination` component accepts as props the pagination variables you defined earlier.

### Test UI Route

To test the UI route out, start the Medusa application, go to `localhost:9000/app`, and log in as an admin user.

Once you log in, you’ll find a new sidebar item, “Digital Products.” If you click on it, you’ll see the UI route you created with a table of digital products.

### Further Reads

- [How to Create UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes)
- [How to Create Admin Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets)

***

## Step 10: Add Create Digital Product Form in Admin

In this step, you’ll add a form for admins to create digital products. The form opens in a drawer or side window from within the Digital Products UI route you created in the previous section.

Create the file `src/admin/components/create-digital-product-form/index.tsx` with the following content:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
import { useState } from "react"
import { Input, Button, Select, toast } from "@medusajs/ui"
import { MediaType } from "../../types"

type CreateMedia = {
  type: MediaType
  file?: File
}

type Props = {
  onSuccess?: () => void
}

const CreateDigitalProductForm = ({
  onSuccess,
}: Props) => {
  const [name, setName] = useState("")
  const [medias, setMedias] = useState<CreateMedia[]>([])
  const [productTitle, setProductTitle] = useState("")
  const [loading, setLoading] = useState(false)

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    // TODO handle submit
  }

  return (
    <form onSubmit={onSubmit}>
      {/* TODO show form inputs */}
      <Button 
        type="submit"
        isLoading={loading}
      >
        Create
      </Button>
    </form>
  )
}

export default CreateDigitalProductForm
```

This creates a React component that shows a form and handles creating a digital product on form submission.

You currently don’t display the form. Replace the return statement with the following:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
return (
  <form onSubmit={onSubmit}>
    <Input
      name="name"
      placeholder="Name"
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
    <fieldset className="my-4">
      <legend className="mb-2">Media</legend>
      <Button type="button" onClick={onAddMedia}>Add Media</Button>
      {medias.map((media, index) => (
        <fieldset className="my-2 p-2 border-solid border rounded">
          <legend>Media {index + 1}</legend>
          <Select 
            value={media.type} 
            onValueChange={(value) => changeFiles(
              index,
              {
                type: value as MediaType,
              }
            )}
          >
            <Select.Trigger>
              <Select.Value placeholder="Media Type" />
            </Select.Trigger>
            <Select.Content>
              <Select.Item value={MediaType.PREVIEW}>
                Preview
              </Select.Item>
              <Select.Item value={MediaType.MAIN}>
                Main
              </Select.Item>
            </Select.Content>
          </Select>
          <Input
            name={`file-${index}`}
            type="file"
            onChange={(e) => changeFiles(
              index,
              {
                file: e.target.files?.[0],
              }
            )}
            className="mt-2"
          />
        </fieldset>
      ))}
    </fieldset>
    <fieldset className="my-4">
      <legend className="mb-2">Product</legend>
      <Input
        name="product_title"
        placeholder="Product Title"
        type="text"
        value={productTitle}
        onChange={(e) => setProductTitle(e.target.value)}
      />
    </fieldset>
    <Button 
      type="submit"
      isLoading={loading}
    >
      Create
    </Button>
  </form>
)
```

This shows input fields for the digital product and product’s names. It also shows a fieldset of media files, with the ability to add more media files on a button click.

Add in the component the `onAddMedia` function that is triggered by a button click to add a new media:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
const onAddMedia = () => {
  setMedias((prev) => [
    ...prev,
    {
      type: MediaType.PREVIEW,
    },
  ])
}
```

And add in the component a `changeFiles` function that saves changes related to a media in the `medias` state variable:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
const changeFiles = (
  index: number,
  data: Partial<CreateMedia>
) => {
  setMedias((prev) => [
    ...(prev.slice(0, index)),
    {
      ...prev[index],
      ...data,
    },
    ...(prev.slice(index + 1)),
  ])
}
```

On submission, the media files should first be uploaded before the digital product is created.

So, add before the `onSubmit` function the following new function:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
const uploadMediaFiles = async (
  type: MediaType
) => {
  const formData = new FormData()
  const mediaWithFiles = medias.filter(
    (media) => media.file !== undefined && 
      media.type === type
  )

  if (!mediaWithFiles.length) {
    return
  }

  mediaWithFiles.forEach((media) => {
      if (!media.file) {
        return
      }
    formData.append("files", media.file)
  })

  const { files } = await fetch(`/admin/digital-products/upload/${type}`, {
    method: "POST",
    credentials: "include",
    body: formData,
  }).then((res) => res.json())

  return {
    mediaWithFiles,
    files,
  }
}
```

This function accepts a type of media to upload (`preview` or `main`). In the function, you upload the files of the specified type using the API route you created in step 7. You return the uploaded files and their associated media.

Next, you’ll implement the `onSubmit` function. Replace it with the following:

```tsx title="src/admin/components/create-digital-product-form/index.tsx" highlights={uploadMediaHighlights}
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault()
  setLoading(true)

  try {
    const {
      mediaWithFiles: previewMedias,
      files: previewFiles,
    } = await uploadMediaFiles(MediaType.PREVIEW) || {}
    const {
      mediaWithFiles: mainMedias,
      files: mainFiles,
    } = await uploadMediaFiles(MediaType.MAIN) || {}

    const mediaData: {
        type: MediaType
        file_id: string
        mime_type: string
      }[] = []

    previewMedias?.forEach((media, index) => {
      mediaData.push({
        type: media.type,
        file_id: previewFiles[index].id,
        mime_type: media.file!.type,
      })
    })

    mainMedias?.forEach((media, index) => {
      mediaData.push({
        type: media.type,
        file_id: mainFiles[index].id,
        mime_type: media.file!.type,
      })
    })

    // TODO create digital product
  } catch (e) {
    console.error(e)
    setLoading(false)
  }
}
```

In this function, you use the `uploadMediaFiles` function to upload `preview` and `main` media files. Then, you prepare the media data that’ll be used when creating the digital product in a `mediaData` variable.

Notice that you use the `id` of uploaded files, as returned in the response of `/admin/digital-products/upload/[type]` as the `file_id` value of the media to be created.

Finally, replace the new `TODO` in `onSubmit` with the following:

```tsx title="src/admin/components/create-digital-product-form/index.tsx"
fetch(`/admin/digital-products`, {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name,
    medias: mediaData,
    product: {
      title: productTitle,
      options: [{
        title: "Default",
        values: ["default"],
      }],
      variants: [{
        title: productTitle,
        options: {
          Default: "default",
        },
        manage_inventory: false,
        // delegate setting the prices to the
        // product's page.
        prices: [],
        shipping_profile_id: "",
      }],
    },
  }),
})
.then((res) => res.json())
.then(({ message }) => {
  if (message) {
    throw message
  }
  onSuccess?.()
})
.catch((e) => {
  console.error(e)
  toast.error("Error", {
    description: `An error occurred while creating the digital product: ${e}`,
  })
})
.finally(() => setLoading(false))
```

In this snippet, you send a `POST` request to `/admin/digital-products` to create a digital product.

You’ll make changes now to `src/admin/routes/digital-products/page.tsx` to show the form.

First, add a new `open` state variable:

```tsx title="src/admin/routes/digital-products/page.tsx"
const DigitalProductsPage = () => {
  const [open, setOpen] = useState(false)
  // ...
}
```

Then, replace the `TODO add create button` in the return statement to show the `CreateDigitalProductForm` component:

```tsx title="src/admin/routes/digital-products/page.tsx"
// other imports...
import { Drawer } from "@medusajs/ui"
import CreateDigitalProductForm from "../../components/create-digital-product-form"

const DigitalProductsPage = () => {
  // ...
    
  return (
    <Container>
      {/* Replace the TODO with the following */}
      <Drawer open={open} onOpenChange={(openChanged) => setOpen(openChanged)}>
        <Drawer.Trigger 
          onClick={() => {
            setOpen(true)
          }}
          asChild
        >
          <Button>Create</Button>
        </Drawer.Trigger>
        <Drawer.Content>
          <Drawer.Header>
            <Drawer.Title>Create Product</Drawer.Title>
          </Drawer.Header>
          <Drawer.Body>
            <CreateDigitalProductForm onSuccess={() => {
              setOpen(false)
              if (currentPage === 0) {
                fetchProducts()
              } else {
                setCurrentPage(0)
              }
            }} />
          </Drawer.Body>
        </Drawer.Content>
      </Drawer>
    </Container>
  )
}
```

This adds a Create button in the Digital Products UI route and, when it’s clicked, shows the form in a drawer or side window.

You pass to the `CreateDigitalProductForm` component an `onSuccess` prop that, when the digital product is created successfully, re-fetches the digital products.

### Test Create Form Out

To test the form, open the Digital Products page in the Medusa Admin. There, you’ll find a new Create button.

If you click on the button, a form will open in a drawer. Fill in the details of the digital product to create one.

After you create the digital product, you’ll find it in the table. You can also click on View Product to edit the product’s details, such as the variant’s price.

To use this digital product in later steps (such as to create an order), you must make the following changes to its associated product details:

1. Change the status to published.
2. Add it to the default sales channel.
3. Add prices to the variant.

***

## Step 11: Handle Product Deletion

When a product is deleted, its product variants are also deleted, meaning that their associated digital products should also be deleted.

In this step, you'll build a flow that deletes the digital products associated with a deleted product's variants. Then, you'll execute this workflow whenever a product is deleted.

The workflow has the following steps:

- `retrieveDigitalProductsToDeleteStep`: Retrieve the digital products associated with a deleted product's variants.
- `deleteDigitalProductsStep`: Delete the digital products.

### retrieveDigitalProductsToDeleteStep

The first step of the workflow receives the ID of the deleted product as an input and retrieves the digital products associated with its variants.

Create the file `src/workflows/delete-product-digital-products/steps/retrieve-digital-products-to-delete.ts` with the following content:

```ts title="src/workflows/delete-product-digital-products/steps/retrieve-digital-products-to-delete.ts" highlights={retrieveDigitalProductsHighlights}
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import DigitalProductVariantLink from "../../../links/digital-product-variant"

type RetrieveDigitalProductsToDeleteStepInput = {
  product_id: string
}

export const retrieveDigitalProductsToDeleteStep = createStep(
  "retrieve-digital-products-to-delete",
  async ({ product_id }: RetrieveDigitalProductsToDeleteStepInput, { container }) => {
    const productService = container.resolve("product")
    const query = container.resolve("query")

    const productVariants = await productService.listProductVariants({
      product_id: product_id,
    }, {
      withDeleted: true,
    })

    const { data } = await query.graph({
      entity: DigitalProductVariantLink.entryPoint,
      fields: ["digital_product.*"],
      filters: {
        product_variant_id: productVariants.map((v) => v.id),
      },
    })

    const digitalProductIds = data.map((d) => d.digital_product.id)

    return new StepResponse(digitalProductIds)
  }
)
```

You create a `retrieveDigitalProductsToDeleteStep` step that retrieves the product variants of the deleted product. Notice that you pass in the second object parameter of `listProductVariants` a `withDeleted` property that ensures deleted variants are included in the result.

Then, you use Query to retrieve the digital products associated with the product variants. Links created with `defineLink` have an `entryPoint` property that you can use with Query to retrieve data from the pivot table of the link between the data models.

Finally, you return the IDs of the digital products to delete.

### deleteDigitalProductsSteps

Next, you'll implement the step that deletes those digital products.

Create the file `src/workflows/delete-product-digital-products/steps/delete-digital-products.ts` with the following content:

```ts title="src/workflows/delete-product-digital-products/steps/delete-digital-products.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"
import DigitalProductModuleService from "../../../modules/digital-product/service"

type DeleteDigitalProductsStep = {
  ids: string[]
}

export const deleteDigitalProductsSteps = createStep(
  "delete-digital-products",
  async ({ ids }: DeleteDigitalProductsStep, { container }) => {
    const digitalProductService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    await digitalProductService.softDeleteDigitalProducts(ids)

    return new StepResponse({}, ids)
  },
  async (ids, { container }) => {
    if (!ids) {
      return
    }

    const digitalProductService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    await digitalProductService.restoreDigitalProducts(ids)
  }
)
```

In the `deleteDigitalProductsSteps`, you soft delete the digital products by the ID passed as a parameter. In the compensation function, you restore the digital products if an error occurs.

### Create deleteProductDigitalProductsWorkflow

You can now create the workflow that executes those steps.

Create the file `src/workflows/delete-product-digital-products/index.ts` with the following content:

```ts title="src/workflows/delete-product-digital-products/index.ts"
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { deleteDigitalProductsSteps } from "./steps/delete-digital-products"
import { retrieveDigitalProductsToDeleteStep } from "./steps/retrieve-digital-products-to-delete"

type DeleteProductDigitalProductsInput = {
  id: string
}

export const deleteProductDigitalProductsWorkflow = createWorkflow(
  "delete-product-digital-products",
  (input: DeleteProductDigitalProductsInput) => {
    const digitalProductsToDelete = retrieveDigitalProductsToDeleteStep({
      product_id: input.id,
    })

    deleteDigitalProductsSteps({
      ids: digitalProductsToDelete,
    })

    return new WorkflowResponse({})
  }
)
```

The `deleteProductDigitalProductsWorkflow` receives the ID of the deleted product as an input. In the workflow, you:

- Run the `retrieveDigitalProductsToDeleteStep` to retrieve the digital products associated with the deleted product.
- Run the `deleteDigitalProductsSteps` to delete the digital products.

### Execute Workflow on Product Deletion

When a product is deleted, Medusa emits a `product.deleted` event. You can handle this event with a subscriber. A subscriber is an asynchronous function that, when an event is emitted, is executed. You can implement in subscribers features that aren't essential to the original flow that emitted the event.

Learn more about subscribers in [this documentation](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers).

So, you'll listen to the `product.deleted` event in a subscriber, and execute the workflow whenever the product is deleted.

Create the file `src/subscribers/handle-product-deleted.ts` with the following content:

```ts title="src/subscribers/handle-product-deleted.ts"
import { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
import { 
  deleteProductDigitalProductsWorkflow,
} from "../workflows/delete-product-digital-products"

export default async function handleProductDeleted({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await deleteProductDigitalProductsWorkflow(container)
    .run({
      input: data,
    })
}

export const config: SubscriberConfig = {
  event: "product.deleted",
}
```

A subscriber file must export:

- An asynchronous function that's executed whenever the specified event is emitted.
- A configuration object that specifies the event the subscriber listens to, which is in this case `product.deleted`.

The subscriber function receives as a parameter an object having the following properties:

- `event`: An object containing the data payload of the emitted event.
- `container`: Instance of the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container).

In the subscriber, you execute the workflow by invoking it, passing the Medusa container as an input, then executing its `run` method. You pass the product's ID, which is received through the event's data payload, as an input to the workflow.

### Test it Out

To test this out, start the Medusa application and, from the Medusa Admin dashboard, delete a product that has digital products. You can confirm that the digital product was deleted by checking the Digital Products page.

***

## Step 12: Create Digital Product Fulfillment Module Provider

In this step, you'll create a fulfillment module provider for digital products. It doesn't have any real fulfillment functionality as digital products aren't physically fulfilled.

### Create Module Provider Service

Start by creating the `src/modules/digital-product-fulfillment` directory.

Then, create the file `src/modules/digital-product-fulfillment/service.ts` with the following content:

```ts title="src/modules/digital-product-fulfillment/service.ts"
import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils"
import { 
  CreateFulfillmentResult, 
  FulfillmentDTO, 
  FulfillmentItemDTO, 
  FulfillmentOption, 
  FulfillmentOrderDTO,
} from "@medusajs/framework/types"

class DigitalProductFulfillmentService extends AbstractFulfillmentProviderService {
  static identifier = "digital"

  constructor() {
    super()
  }

  async getFulfillmentOptions(): Promise<FulfillmentOption[]> {
    return [
      {
        id: "digital-fulfillment",
      },
    ]
  }

  async validateFulfillmentData(
    optionData: Record<string, unknown>,
    data: Record<string, unknown>,
    context: Record<string, unknown>
  ): Promise<any> {
    return data
  }

  async validateOption(data: Record<string, any>): Promise<boolean> {
    return true
  }

  async createFulfillment(
    data: Record<string, unknown>, 
    items: Partial<Omit<FulfillmentItemDTO, "fulfillment">>[], 
    order: Partial<FulfillmentOrderDTO> | undefined, 
    fulfillment: Partial<Omit<FulfillmentDTO, "provider_id" | "data" | "items">>
  ): Promise<CreateFulfillmentResult> {
    // No data is being sent anywhere
    return {
      data,
      labels: [],
    }
  }

  async cancelFulfillment(): Promise<any> {
    return {}
  }

  async createReturnFulfillment(): Promise<any> {
    return {}
  }
}

export default DigitalProductFulfillmentService
```

The fulfillment provider registers one fulfillment option, and doesn't perform actual fulfillment.

### Create Module Provider Definition

Then, create the module provider's definition in the file `src/modules/digital-product-fulfillment/index.ts`:

```ts title="src/modules/digital-product-fulfillment/index.ts"
import { ModuleProviderExports } from "@medusajs/framework/types"
import DigitalProductFulfillmentService from "./service"

const services = [DigitalProductFulfillmentService]

const providerExport: ModuleProviderExports = {
  services,
}

export default providerExport
```

### Register Module Provider in Medusa's Configurations

Finally, register the module provider in `medusa-config.ts`:

```ts title="medusa-config.ts"
// other imports...
import { Modules } from "@medusajs/framework/utils"

module.exports = defineConfig({
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/fulfillment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/fulfillment-manual",
            id: "manual",
          },
          {
            resolve: "./src/modules/digital-product-fulfillment",
            id: "digital",
          },
        ],
      },
    },
  ],
})
```

This registers the digital product fulfillment as a module provider of the Fulfillment Module.

### Add Fulfillment Provider to Location

In the Medusa Admin, go to Settings -> Location & Shipping, and add the fulfillment provider and a shipping option for it in a location.

This is necessary to use the fulfillment provider's shipping option during checkout.

***

## Step 13: Create Cart Completion Flow for Digital Products

In this step, you’ll create a new cart completion flow that not only creates a Medusa order, but also create a digital product order.

To create the cart completion flow, you’ll create a workflow and then use that workflow in an API route defined at `src/api/store/carts/[id]/complete-digital/route.ts`.

```mermaid
graph TD
  completeCartWorkflow["completeCartWorkflow (Medusa)"] --> useQueryGraphStep["useQueryGraphStep (Medusa)"]
  useQueryGraphStep --> when{order has digital products?}
  when -->|Yes| createDigitalProductOrderStep
  createDigitalProductOrderStep --> createRemoteLinkStep["createRemoteLinkStep (Medusa)"]
  createRemoteLinkStep --> createOrderFulfillmentWorkflow["createOrderFulfillmentWorkflow (Medusa)"]
  createOrderFulfillmentWorkflow --> emitEventStep["emitEventStep (Medusa)"]
  emitEventStep --> End
  when -->|No| End
```

The workflow has the following steps:

1. `completeCartWorkflow` to create a Medusa order from the cart. Medusa provides this workflow through the `@medusajs/medusa/core-flows` package and you can use it as a step.
2. `useQueryGraphStep` to retrieve the order’s items with the digital products associated with the purchased product variants. Medusa provides this step through the `@medusajs/medusa/core-flows` package.
3. If the order has digital products, you:
   1. create the digital product order.
   2. link the digital product order with the Medusa order. Medusa provides a `createRemoteLinkStep` in the `@medusajs/medusa/core-flows` package that can be used here.
   3. Create a fulfillment for the digital products in the order. Medusa provides a `createOrderFulfillmentWorkflow` in the `@medusajs/medusa/core-flows` package that you can use as a step here.
   4. Emit the `digital_product_order.created` custom event to handle it later in a subscriber and send the customer an email. Medusa provides a `emitEventStep` in the `@medusajs/medusa/core-flows` that you can use as a step here.

You’ll only implement the `3.a` step of the workflow.

### createDigitalProductOrderStep (Step 3.a)

Create the file `src/workflows/create-digital-product-order/steps/create-digital-product-order.ts` with the following content:

```ts title="src/workflows/create-digital-product-order/steps/create-digital-product-order.ts" highlights={createDpoHighlights} collapsibleLines="1-14" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  OrderLineItemDTO,
  ProductVariantDTO,
  InferTypeOf,
} from "@medusajs/framework/types"
import { OrderStatus } from "../../../modules/digital-product/types"
import DigitalProductModuleService from "../../../modules/digital-product/service"
import { DIGITAL_PRODUCT_MODULE } from "../../../modules/digital-product"
import DigitalProduct from "../../../modules/digital-product/models/digital-product"

export type CreateDigitalProductOrderStepInput = {
  items: (OrderLineItemDTO & {
    variant: ProductVariantDTO & {
      digital_product: InferTypeOf<typeof DigitalProduct>
    }
  })[]
}

const createDigitalProductOrderStep = createStep(
  "create-digital-product-order",
  async ({ items }: CreateDigitalProductOrderStepInput, { container }) => {
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    const digitalProductIds = items.map((item) => item.variant.digital_product.id)

    const digitalProductOrder = await digitalProductModuleService
      .createDigitalProductOrders({
        status: OrderStatus.PENDING,
        products: digitalProductIds,
      })

    return new StepResponse({
      digital_product_order: digitalProductOrder,
    }, {
      digital_product_order: digitalProductOrder,
    })
  },
  async (data, { container }) => {
    if (!data) {
      return
    }
    const digitalProductModuleService: DigitalProductModuleService = 
      container.resolve(DIGITAL_PRODUCT_MODULE)

    await digitalProductModuleService.deleteDigitalProductOrders(
      data.digital_product_order.id
    )
  }
)

export default createDigitalProductOrderStep
```

This creates the `createDigitalProductOrderStep`. In this step, you create a digital product order.

In the compensation function, you delete the digital product order.

### Create createDigitalProductOrderWorkflow

Create the file `src/workflows/create-digital-product-order/index.ts` with the following content:

```ts title="src/workflows/create-digital-product-order/index.ts" highlights={createDpoWorkflowHighlights} collapsibleLines="1-17" expandMoreLabel="Show Imports"
import {
  createWorkflow,
  transform,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  completeCartWorkflow,
  useQueryGraphStep,
  createRemoteLinkStep,
  createOrderFulfillmentWorkflow,
  emitEventStep,
  acquireLockStep,
  releaseLockStep,
} from "@medusajs/medusa/core-flows"
import {
  Modules,
} from "@medusajs/framework/utils"
import createDigitalProductOrderStep, { 
  CreateDigitalProductOrderStepInput,
} from "./steps/create-digital-product-order"
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"
import digitalProductOrderOrderLink from "../../links/digital-product-order"

type WorkflowInput = {
  cart_id: string
}

const createDigitalProductOrderWorkflow = createWorkflow(
  "create-digital-product-order",
  (input: WorkflowInput) => {
    acquireLockStep({
      key: input.cart_id,
      timeout: 30,
      ttl: 120,
    })
    const { id } = completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    })

    const { data: orders } = useQueryGraphStep({
      entity: "order",
      fields: [
        "*",
        "items.*",
        "items.variant.*",
        "items.variant.digital_product.*",
        "shipping_address.*",
      ],
      filters: {
        id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    const { data: existingLinks } = useQueryGraphStep({
      entity: digitalProductOrderOrderLink.entryPoint,
      fields: ["digital_product_order.id"],
      filters: { order_id: id },
    }).config({ name: "retrieve-existing-links" })

    const itemsWithDigitalProducts = transform(
      {
        orders,
      },
      (data) => {
        return data.orders[0].items?.filter(
          (item) => item?.variant?.digital_product !== undefined
        )
      }
    )

    const digital_product_order = when(
      "create-digital-product-order-condition",
      { itemsWithDigitalProducts, existingLinks },
      (data) => {
        return (
          !!data.itemsWithDigitalProducts?.length &&
          data.existingLinks.length === 0
        )
      }
    ).then(() => {
      const { 
        digital_product_order,
      } = createDigitalProductOrderStep({
        items: orders[0].items,
      } as unknown as CreateDigitalProductOrderStepInput)
  
      createRemoteLinkStep([{
        [DIGITAL_PRODUCT_MODULE]: {
          digital_product_order_id: digital_product_order.id,
        },
        [Modules.ORDER]: {
          order_id: id,
        },
      }])

      createOrderFulfillmentWorkflow.runAsStep({
        input: {
          order_id: id,
          items: transform({
            itemsWithDigitalProducts,
          }, (data) => {
            return data.itemsWithDigitalProducts!.map((item) => ({
              id: item!.id,
              quantity: item!.quantity,
            }))
          }),
        },
      })
  
      emitEventStep({
        eventName: "digital_product_order.created",
        data: {
          id: digital_product_order.id,
        },
      })

      return digital_product_order
    })

    releaseLockStep({
      key: input.cart_id,
    })

    return new WorkflowResponse({
      order: orders[0],
      digital_product_order,
    })
  }
)

export default createDigitalProductOrderWorkflow
```

This creates the workflow `createDigitalProductOrderWorkflow`. It runs the following steps:

1. `acquireLockStep` to acquire a lock on the cart ID.
2. `completeCartWorkflow` as a step to create the Medusa order.
3. `useQueryGraphStep` to retrieve the order’s items with their associated variants and linked digital products.
4. `useQueryGraphStep` to retrieve any existing links between the digital product order and the Medusa order.
   - This is necessary to ensure the workflow is idempotent, meaning that if the workflow is executed multiple times for the same cart, it doesn’t create multiple digital product orders.
5. Use `transform` to filter the order’s items to only those having digital products.
6. Use `when` to check whether the order has digital products and no existing links. If so:
   1. Use the `createDigitalProductOrderStep` to create the digital product order.
   2. Use the `createRemoteLinkStep` to link the digital product order to the Medusa order.
   3. Use the `createOrderFulfillmentWorkflow` to create a fulfillment for the digital products in the order.
   4. Use the `emitEventStep` to emit a custom event.
7. `releaseLockStep` to release the lock on the cart ID.

The workflow returns the Medusa order and the digital product order, if created.

### Cart Completion API Route

Next, create the file `src/api/store/carts/[id]/complete-digital/route.ts` with the following content:

```ts title="src/api/store/carts/[id]/complete-digital/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import createDigitalProductOrderWorkflow from "../../../../../workflows/create-digital-product-order"

export const POST = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const { result } = await createDigitalProductOrderWorkflow(req.scope)
    .run({
      input: {
        cart_id: req.params.id,
      },
    })

  res.json({
    type: "order",
    ...result,
  })
}
```

Since you export a `POST` function, you expose a `POST` API route at `/store/carts/[id]/complete-digital`.

In the route handler, you execute the `createDigitalProductOrderWorkflow` and return the created order in the response.

### Test Cart Completion: Customize Next.js Starter Storefront

To test out the cart completion, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/nextjs-starter) that you installed in the first step to use the new cart completion route to place an order.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-digital-product`, you can find the storefront by going back to the parent directory and changing to the `medusa-digital-product-storefront` directory:

```bash
cd ../medusa-digital-product-storefront # change based on your project name
```

In the Next.js Starter Storefront, open the file `src/lib/data/cart.ts` and find the following lines in the `placeOrder` function:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const cartRes = await sdk.store.cart
  .complete(id, {}, headers)
```

Replace these lines with the following:

```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="blue"
const cartRes = await sdk.client.fetch<HttpTypes.StoreCompleteCartResponse>(
  `/store/carts/${id}/complete-digital`,
    {
      method: "POST",
      headers,
    }
  )
```

This will send a `POST` request to the new cart completion route you created to complete the cart.

Then, run the following command in the Medusa application directory to start the Medusa application:

```bash npm2yarn badgeLabel="Medusa application" badgeColor="green"
npm run start
```

And run the following command in the Next.js Starter Storefront directory to start the Next.js application:

```bash npm2yarn badgeLabel="Storefront" badgeColor="blue"
npm run dev
```

Open the storefront in your browser at `http://localhost:8000` and add a digital product to the cart.

Then, go through the checkout process. Make sure to choose the shipping option you created in the previous step for shipping.

Once you place the order, the cart completion route you added above will run, creating the order and digital product order, if the order has digital products.

In a later step, you’ll add an API route to allow customers to view and download their purchased digital products.

### Further Read

- [Conditions in Workflows with When-Then](https://docs.medusajs.com/learn/fundamentals/workflows/conditions)

***

## Step 14: Fulfill Digital Order Workflow

In this step, you'll create a workflow that fulfills a digital order by sending a notification to the customer. Later, you'll execute this workflow in a subscriber that listens to the `digital_product_order.created` event.

The workflow has the following steps:

1. Retrieve the digital product order's details. For this, you'll use `useQueryGraphStep` from Medusa's core workflows.
2. Send a notification to the customer with the digital products to download.
3. Mark the Medusa order's fulfillment as delivered. For this, you'll use `markOrderFulfillmentAsDeliveredWorkflow` from Medusa's core workflows.

So, you only need to implement the second step.

### Add Types

Before creating the step, add to `src/modules/digital-product/types/index.ts` the following:

```ts
import { OrderDTO, InferTypeOf } from "@medusajs/framework/types"
import DigitalProductOrder from "../models/digital-product-order"

// ...

export type DigitalProductOrder = 
  InferTypeOf<typeof DigitalProductOrder> & {
    order?: OrderDTO
  }
```

This adds a type for a digital product order, which you'll use next.

You use `InferTypeOf` to infer the type of the `DigitalProductOrder` data model, and add to it the optional `order` property, which is the linked order.

### Create sendDigitalOrderNotificationStep

To create the step, create the file `src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts` with the following content:

```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts" collapsibleLines="1-11" expandMoreLabel="Show Imports"
import {
  createStep,
  StepResponse,
} from "@medusajs/framework/workflows-sdk"
import { 
  INotificationModuleService,
  IFileModuleService,
} from "@medusajs/framework/types"
import { 
  MedusaError, 
  ModuleRegistrationName, 
  promiseAll,
} from "@medusajs/framework/utils"
import { DigitalProductOrder, MediaType } from "../../../modules/digital-product/types"

export type SendDigitalOrderNotificationStepInput = {
  digital_product_order: DigitalProductOrder
}

export const sendDigitalOrderNotificationStep = createStep(
  "send-digital-order-notification",
  async ({ 
    digital_product_order: digitalProductOrder, 
  }: SendDigitalOrderNotificationStepInput, 
  { container }) => {
    const notificationModuleService: INotificationModuleService = container
    .resolve(ModuleRegistrationName.NOTIFICATION)
    const fileModuleService: IFileModuleService = container.resolve(
      ModuleRegistrationName.FILE
    )

    if (!digitalProductOrder.order) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Digital product order is missing associated order."
      )
    }

    if (!digitalProductOrder.order.email) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "Order is missing email."
      )
    }

    // TODO assemble notification
  }
)
```

This creates the `sendDigitalOrderNotificationStep` step that receives a digital product order as an input.

In the step, so far you resolve the main services of the Notification and File Modules. You also check that the digital product order has an associated order and that the order has an email.

Next, you'll prepare the data to pass in the notification. Replace the `TODO` with the following:

```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts"
const notificationData = await promiseAll(
  digitalProductOrder.products.map(async (product) => {
    const medias: string[] = []

    await promiseAll(
      product.medias
      .filter((media) => media.type === MediaType.MAIN)
      .map(async (media) => {
        medias.push(
          (await fileModuleService.retrieveFile(media.fileId)).url
        )
      })
    )

    return {
      name: product.name,
      medias,
    }
  })
)

// TODO send notification
```

In this snippet, you put together the data to send in the notification. You loop over the digital products in the order and retrieve the URL of their main files using the File Module.

Finally, replace the new `TODO` with the following:

```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts"
const notification = await notificationModuleService.createNotifications({
  to: digitalProductOrder.order.email,
  template: "digital-order-template",
  channel: "email",
  data: {
    products: notificationData,
  },
})

return new StepResponse(notification)
```

You use the `createNotifications` method of the Notification Module's main service to send an email using the installed provider.

### Create Workflow

Create the workflow in the file `src/workflows/fulfill-digital-order/index.ts`:

```ts title="src/workflows/fulfill-digital-order/index.ts" highlights={fulfillWorkflowHighlights} collapsibleLines="1-13" expandMoreLabel="Show Imports"
import {
  createWorkflow,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
  markOrderFulfillmentAsDeliveredWorkflow,
  useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { 
  sendDigitalOrderNotificationStep, 
  SendDigitalOrderNotificationStepInput,
} from "./steps/send-digital-order-notification"

type FulfillDigitalOrderWorkflowInput = {
  id: string
}

export const fulfillDigitalOrderWorkflow = createWorkflow(
  "fulfill-digital-order",
  ({ id }: FulfillDigitalOrderWorkflowInput) => {
    const { data: digitalProductOrders } = useQueryGraphStep({
      entity: "digital_product_order",
      fields: [
        "*",
        "products.*",
        "products.medias.*",
        "order.*",
        "order.fulfillments.*",
      ],
      filters: {
        id,
      },
      options: {
        throwIfKeyNotFound: true,
      },
    })

    sendDigitalOrderNotificationStep({
      digital_product_order: digitalProductOrders[0],
    } as unknown as SendDigitalOrderNotificationStepInput)

    when({ digitalProductOrders }, (data) => !!data.digitalProductOrders[0].order?.fulfillments?.length)
      .then(() => {
        markOrderFulfillmentAsDeliveredWorkflow.runAsStep({
          input: {
            orderId: digitalProductOrders[0].order!.id,
            fulfillmentId: digitalProductOrders[0].order!.fulfillments![0]!.id,
          },
        })
      })

    return new WorkflowResponse(
      digitalProductOrders[0]
    )
  }
)
```

In the workflow, you:

1. Retrieve the digital product order's details using `useQueryGraphStep` from Medusa's core workflows.
2. Send a notification to the customer with the digital product download links using the `sendDigitalOrderNotificationStep`.
3. Check whether the Medusa order has fulfillments. If so, mark the order's fulfillment as delivered using `markOrderFulfillmentAsDeliveredWorkflow` from Medusa's core workflows.

`when` allows you to perform steps based on a condition during execution. Learn more in the [Conditions in Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/conditions) documentation.

### Configure Notification Module Provider

In the `sendDigitalOrderNotificationStep`, you use a notification provider configured for the `email` channel to send the notification.

Check out the [Integrations page](https://docs.medusajs.com/integrations) to find Notification Module Providers.

For testing purposes, add to `medusa-config.ts` the following to use the Local Notification Module Provider:

```ts title="medusa-config.ts"
module.exports = defineConfig({
  // ...
  modules: [
    // ...
    {
      resolve: "@medusajs/medusa/notification",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/notification-local",
            id: "local",
            options: {
              name: "Local Notification Provider",
              channels: ["email"],
            },
          },
        ],
      },
    },
  ],
})

```

***

## Step 15: Handle the Digital Product Order Event

In this step, you'll create a subscriber that listens to the `digital_product_order.created` event and executes the workflow from the above step.

Create the file `src/subscribers/handle-digital-order.ts` with the following content:

```ts title="src/subscribers/handle-digital-order.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports"
import type {
  SubscriberArgs,
  SubscriberConfig,
} from "@medusajs/framework"
import { 
  fulfillDigitalOrderWorkflow,
} from "../workflows/fulfill-digital-order"

async function digitalProductOrderCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  await fulfillDigitalOrderWorkflow(container).run({
    input: {
      id: data.id,
    },
  })
}

export default digitalProductOrderCreatedHandler

export const config: SubscriberConfig = {
  event: "digital_product_order.created",
}
```

This adds a subscriber that listens to the `digital_product_order.created` event. It executes the `fulfillDigitalOrderWorkflow` to send the customer an email and mark the order's fulfillment as fulfilled.

### Test Subscriber Out

To test out the subscriber, place an order with digital products. This triggers the `digital_product_order.created` event which executes the subscriber.

***

## Step 16: Create Store API Routes

In this step, you’ll create three store API routes:

1. Retrieve the preview files of a digital product. This is useful when the customer is browsing the products before purchase.
2. List the digital products that the customer has purchased.
3. Get the download link to a media of the digital product that the customer purchased.

### Retrieve Digital Product Previews API Route

Create the file `src/api/store/digital-products/[id]/preview/route.ts` with the following content:

```ts title="src/api/store/digital-products/[id]/preview/route.ts" highlights={previewRouteHighlights} collapsibleLines="1-15" expandMoreLabel="Show Imports"
import { 
  MedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import {
  Modules,
} from "@medusajs/framework/utils"
import { 
  DIGITAL_PRODUCT_MODULE,
} from "../../../../../modules/digital-product"
import DigitalProductModuleService from "../../../../../modules/digital-product/service"
import { 
  MediaType,
} from "../../../../../modules/digital-product/types"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  const fileModuleService = req.scope.resolve(
    Modules.FILE
  )

  const digitalProductModuleService: DigitalProductModuleService = 
    req.scope.resolve(
      DIGITAL_PRODUCT_MODULE
    )
  
  const medias = await digitalProductModuleService.listDigitalProductMedias({
    digital_product_id: req.params.id,
    type: MediaType.PREVIEW,
  })

  const normalizedMedias = await Promise.all(
    medias.map(async (media) => {
      const { fileId, ...mediaData } = media
      const fileData = await fileModuleService.retrieveFile(fileId)

      return {
        ...mediaData,
        url: fileData.url,
      }
    })
  )

  res.json({
    previews: normalizedMedias,
  })
}
```

This adds a `GET` API route at `/store/digital-products/[id]/preview`, where `[id]` is the ID of the digital product to retrieve its preview media.

In the route handler, you retrieve the preview media of the digital product and then use the File Module’s service to get the URL of the preview file.

You return in the response the preview files.

### List Digital Product Purchases API Route

Create the file `src/api/store/customers/me/digital-products/route.ts` with the following content:

```ts title="src/api/store/customers/me/digital-products/route.ts" highlights={purchasedDpHighlights} collapsibleLines="1-8" expandMoreLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  ContainerRegistrationKeys,
} from "@medusajs/framework/utils"

export const GET = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: [
      "orders.digital_product_order.products.*",
      "orders.digital_product_order.products.medias.*",
    ],
    filters: {
      id: req.auth_context.actor_id,
    },
  })

  const digitalProducts = {}

  customer.orders?.forEach((order) => {
    order?.digital_product_order?.products.forEach((product) => {
      if (!product) {
        return
      }
      digitalProducts[product.id] = product
    })
  })

  res.json({
    digital_products: Object.values(digitalProducts),
  })
}
```

This adds a `GET` API route at `/store/customers/me/digital-products`. All API routes under `/store/customers/me` require customer authentication.

In the route handler, you use Query to retrieve the customer’s orders and linked digital product orders, and return the purchased digital products in the response.

### Get Digital Product Media Download URL API Route

Create the file `src/api/store/customers/me/digital-products/[mediaId]/download/route.ts` with the following content:

```ts title="src/api/store/customers/me/digital-products/[mediaId]/download/route.ts" highlights={downloadUrlHighlights} collapsibleLines="1-10" expandMoreLabel="Show Imports"
import { 
  AuthenticatedMedusaRequest, 
  MedusaResponse,
} from "@medusajs/framework/http"
import { 
  Modules,
  ContainerRegistrationKeys,
  MedusaError,
} from "@medusajs/framework/utils"

export const POST = async (
  req: AuthenticatedMedusaRequest,
  res: MedusaResponse
) => {
  const fileModuleService = req.scope.resolve(
    Modules.FILE
  )
  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)

  const { data: [customer] } = await query.graph({
    entity: "customer",
    fields: [
      "orders.digital_product_order.*",
    ],
    filters: {
      id: req.auth_context.actor_id,
    },
  })

  const customerDigitalOrderIds = customer.orders?.filter(
    (order) => order?.digital_product_order !== undefined
  ).map((order) => order!.digital_product_order!.id)

  const { data: dpoResult } = await query.graph({
    entity: "digital_product_order",
    fields: [
      "products.medias.*",
    ],
    filters: {
      id: customerDigitalOrderIds,
    },
  })

  if (!dpoResult.length) {
    throw new MedusaError(
      MedusaError.Types.NOT_ALLOWED,
      "Customer didn't purchase digital product."
    )
  }

  let foundMedia: any | undefined = undefined

  dpoResult[0].products.some((product) => {
    return product?.medias.some((media) => {
      foundMedia = media?.id === req.params.mediaId ? media : undefined

      return foundMedia !== undefined
    })
  })

  if (!foundMedia) {
    throw new MedusaError(
      MedusaError.Types.NOT_ALLOWED,
      "Customer didn't purchase digital product."
    )
  }

  const fileData = await fileModuleService.retrieveFile(foundMedia.fileId)

  res.json({
    url: fileData.url,
  })
}
```

This adds a `POST` API route at `/store/customers/me/digital-products/[mediaId]`, where `[mediaId]` is the ID of the digital product media to download.

In the route handler, you retrieve the customer’s orders and linked digital orders, then check if the digital orders have the required media file. If not, an error is thrown.

If the media is found in th customer's previous purchases, you use the File Module’s service to retrieve the download URL of the media and return it in the response.

You’ll test out these API routes in the next step.

### Further Reads

- [What are protected API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/protected-routes)

***

## Step 17: Customize Next.js Starter

In this section, you’ll customize the [Next.js Starter storefront](https://docs.medusajs.com/nextjs-starter) to:

1. Show a preview button on a digital product’s page to view the preview files.
2. Add a new tab in the customer’s dashboard to view their purchased digital products.
3. Allow customers to download the digital products through the new page in the dashboard.

The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`.

So, if your Medusa application's directory is `medusa-digital-product`, you can find the storefront by going back to the parent directory and changing to the `medusa-digital-product-storefront` directory:

```bash
cd ../medusa-digital-product-storefront # change based on your project name
```

### Add Types

In `src/types/global.ts`, add the following types that you’ll use in your customizations:

```ts title="src/types/global.ts" badgeLabel="Storefront" badgeColor="blue"
import { 
  // other imports...
  StoreProductVariant,
} from "@medusajs/types"

// ...

export type DigitalProduct = {
  id: string
  name: string
  medias?: DigitalProductMedia[]
}

export type DigitalProductMedia = {
  id: string
  fileId: string
  type: "preview" | "main"
  mimeType: string
  digitalProduct?: DigitalProduct[]
}

export type DigitalProductPreview = DigitalProductMedia & {
  url: string
}

export type VariantWithDigitalProduct = StoreProductVariant & {
  digital_product?: DigitalProduct
}

```

### Retrieve Digital Products with Variants

To retrieve the digital products details when retrieving a product and its variants, in the `src/lib/data/products.ts` file, change the `listProducts` function to pass the digital products in the `fields` property passed to the `sdk.store.product.list` method:

```ts title="src/lib/data/products.ts" highlights={fieldHighlights} badgeLabel="Storefront" badgeColor="blue"
export const listProducts = async ({
  pageParam = 1,
  queryParams,
  countryCode,
  regionId,
}: {
  pageParam?: number
  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
  countryCode?: string
  regionId?: string
}): Promise<{
  response: { products: HttpTypes.StoreProduct[]; count: number }
  nextPage: number | null
  queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams
}> => {
  // ...
  return sdk.client
    .fetch<{ products: HttpTypes.StoreProduct[]; count: number }>(
      `/store/products`,
      {
        // ...
        query: {
          // ...
          fields: "*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,*variants.calculated_price,*variants.digital_product",
        },
      }
    )
    // ...
}
```

When a customer views a product’s details page, digital products linked to variants are also retrieved.

### Get Digital Product Preview Links

To retrieve the links of a digital product’s preview media, first, add the following import at the top of `src/lib/data/products.ts`:

```ts title="src/lib/data/products.ts" badgeLabel="Storefront" badgeColor="blue"
import { DigitalProductPreview } from "../../types/global"
```

Then, add the following function at the end of the file:

```ts title="src/lib/data/products.ts"
export const getDigitalProductPreview = async function ({
  id,
}: {
  id: string
}) {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("products")),
  }
  const { previews } = await sdk.client.fetch<{
    previews: DigitalProductPreview[]
  }>(
    `/store/digital-products/${id}/preview`, 
    {
      headers,
      next,
      cache: "force-cache",
    }
  )

  // for simplicity, return only the first preview url
  // instead you can show all the preview media to the customer
  return previews.length ? previews[0].url : ""
}
```

This function uses the API route you created in the previous section to get the preview links and return the first preview link.

### Add Preview Button

To add a button that shows the customer the preview media of a digital product, first, in `src/modules/products/components/product-actions/index.tsx`, cast the `selectedVariant` variable in the component to the `VariantWithDigitalProduct` type you created earlier:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// other imports...
import { VariantWithDigitalProduct } from "../../../../types/global"

export default function ProductActions({
  product,
  region,
  disabled,
}: ProductActionsProps) {

  // ...
    
  const selectedVariant = useMemo(() => {
    // ...
  }, [product.variants, options]) as VariantWithDigitalProduct
    
  // ...
}
```

Then, add the following function in the component:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// other imports...
import { getDigitalProductPreview } from "../../../../lib/data/products"

export default function ProductActions({
  product,
  region,
  disabled,
}: ProductActionsProps) {
  // ...
    
  const handleDownloadPreview = async () => {
    if (!selectedVariant?.digital_product) {
      return
    }

    const downloadUrl = await getDigitalProductPreview({
      id: selectedVariant?.digital_product.id,
    })

    if (downloadUrl.length) {
      window.open(downloadUrl)
    }
  }
    
  // ...
}
```

This function uses the `getDigitalProductPreview` function you created earlier to retrieve the preview URL of the selected variant’s digital product.

Finally, in the `return` statement, add a new button above the add-to-cart button:

```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
return (
  <div>
    {/* Before add to cart */}
    {selectedVariant?.digital_product && (
      <Button
        onClick={handleDownloadPreview}
        variant="secondary"
        className="w-full h-10"
      >
        Download Preview
      </Button>
      )}
  </div>
)
```

This button is only shown if the selected variant has a digital product. When it’s clicked, the preview URL is retrieved to show the preview media to the customer.

### Test Preview Out

To test it out, run the Next.js starter with the Medusa application, then open the details page of a product that’s digital. You should see a “Download Preview” button to download the preview media of the product.

### Add Digital Purchases Page

You’ll now create the page customers can view their purchased digital product in.

Start by creating the file `src/lib/data/digital-products.ts` with the following content:

```ts title="src/lib/data/digital-products.ts" badgeLabel="Storefront" badgeColor="blue"
"use server"

import { DigitalProduct } from "../../types/global"
import { sdk } from "../config"
import { getAuthHeaders, getCacheOptions } from "./cookies"

export const getCustomerDigitalProducts = async () => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("products")),
  }
  const { digital_products } = await sdk.client.fetch<{
    digital_products: DigitalProduct[]
  }>(`/store/customers/me/digital-products`, {
    
    headers,
    next,
   cache: "no-cache",
  })

  return digital_products as DigitalProduct[]
}
```

The `getCustomerDigitalProducts` retrieves the logged-in customer’s purchased digital products by sending a request to the API route you created earlier.

Then, create the file `src/modules/account/components/digital-products-list/index.tsx` with the following content:

```tsx title="src/modules/account/components/digital-products-list/index.tsx" badgeLabel="Storefront" badgeColor="blue"
"use client"

import { Table } from "@medusajs/ui"
import { DigitalProduct } from "../../../../types/global"

type Props = {
  digitalProducts: DigitalProduct[]
}

export const DigitalProductsList = ({
  digitalProducts,
}: Props) => {
  return (
    <Table>
      <Table.Header>
        <Table.Row>
          <Table.HeaderCell>Name</Table.HeaderCell>
          <Table.HeaderCell>Action</Table.HeaderCell>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        {digitalProducts.map((digitalProduct) => {
          const medias = digitalProduct.medias?.filter((media) => media.type === "main")
          const showMediaCount = (medias?.length || 0) > 1
          return (
            <Table.Row key={digitalProduct.id}>
              <Table.Cell>
                {digitalProduct.name}
              </Table.Cell>
              <Table.Cell>
                <ul>
                  {medias?.map((media, index) => (
                    <li key={media.id}>
                      <a href="#">
                        Download{showMediaCount ? ` ${index + 1}` : ``}
                      </a>
                    </li>
                  ))}
                </ul>
              </Table.Cell>
            </Table.Row>
          )
        })}
      </Table.Body>
    </Table>
  )
}
```

This adds a `DigitalProductsList` component that receives a list of digital products and shows them in a table. Each digital product’s media has a download link. You’ll implement its functionality afterwards.

Next, create the file `src/app/[countryCode]/(main)/account/@dashboard/digital-products/page.tsx` with the following content:

```tsx title="src/app/[countryCode]/(main)/account/@dashboard/digital-products/page.tsx" badgeLabel="Storefront" badgeColor="blue"
import { Metadata } from "next"

import { getCustomerDigitalProducts } from "../../../../../../lib/data/digital-products"
import { DigitalProductsList } from "../../../../../../modules/account/components/digital-products-list"

export const metadata: Metadata = {
  title: "Digital Products",
  description: "Overview of your purchased digital products.",
}

export default async function DigitalProducts() {
  const digitalProducts = await getCustomerDigitalProducts()

  return (
    <div className="w-full" data-testid="orders-page-wrapper">
      <div className="mb-8 flex flex-col gap-y-4">
        <h1 className="text-2xl-semi">Digital Products</h1>
        <p className="text-base-regular">
          View the digital products you've purchased and download them.
        </p>
      </div>
      <div>
        <DigitalProductsList digitalProducts={digitalProducts} />
      </div>
    </div>
  )
}
```

This adds a new route in your Next.js application to show the customer’s purchased digital products.

In the route, you retrieve the digital’s products using the `getCustomerDigitalProducts` function and pass them as the prop of the `DigitalProductsList` component.

Finally, to add a tab in the customer’s account dashboard that links to this page, add it in the `src/modules/account/components/account-nav/index.tsx` file:

```tsx title="src/modules/account/components/account-nav/index.tsx" badgeLabel="Storefront" badgeColor="blue"
// other imports...
import { Photo } from "@medusajs/icons"

const AccountNav = ({
  customer,
}: {
  customer: HttpTypes.StoreCustomer | null
}) => {
  // ...
    
  return (
    <div>
      <div className="small:hidden">
        {/* ... */}
        {/* Add before log out */}
        <li>
          <LocalizedClientLink
            href="/account/digital-products"
            className="flex items-center justify-between py-4 border-b border-gray-200 px-8"
            data-testid="digital-products-link"
          >
            <div className="flex items-center gap-x-2">
              <Photo />
              <span>Digital Products</span>
            </div>
            <ChevronDown className="transform -rotate-90" />
          </LocalizedClientLink>
        </li>
        {/* ... */}
      </div>
      <div className="hidden small:block">
        {/* ... */}
        {/* Add before log out */}
        <li>
          <AccountNavLink
            href="/account/digital-products"
            route={route!}
            data-testid="digital-products-link"
          >
            Digital Products
          </AccountNavLink>
        </li>
        {/* ... */}
      </div>
    </div>
  )
}
```

You add a link to the new route before the log out tab both for small and large devices.

### Test Purchased Digital Products Page

To test out this page, first, log-in as a customer and place an order with a digital product.

Then, go to the customer’s account page and click on the new Digital Products tab. You’ll see a table of digital products to download.

### Add Download Link

To add a download link for the purchased digital products’ medias, first, add a new function to `src/lib/data/digital-products.ts`:

```ts title="src/lib/data/digital-products.ts" badgeLabel="Storefront" badgeColor="blue"
export const getDigitalMediaDownloadLink = async (mediaId: string) => {
  const headers = {
    ...(await getAuthHeaders()),
  }

  const next = {
    ...(await getCacheOptions("products")),
  }
  const { url } = await sdk.client.fetch<{
    url: string
  }>(`/store/customers/me/digital-products/${mediaId}/download`, {
    method: "POST",
    headers,
    next,
   cache: "no-cache",
  })

  return url
}
```

In this function, you send a request to the download API route you created earlier to retrieve the download URL of a purchased digital product media.

Then, in `src/modules/account/components/digital-products-list/index.tsx`, import the `getDigitalMediaDownloadLink` at the top of the file:

```tsx title="src/modules/account/components/digital-products-list/index.tsx" badgeLabel="Storefront" badgeColor="blue"
import { getDigitalMediaDownloadLink } from "../../../../lib/data/digital-products"
```

And add a `handleDownload` function in the `DigitalProductsList` component:

```tsx title="src/modules/account/components/digital-products-list/index.tsx"
const handleDownload = async (
  e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
  mediaId: string
) => {
  e.preventDefault()

  const url = await getDigitalMediaDownloadLink(mediaId)

  window.open(url)
}
```

This function uses the `getDigitalMediaDownloadLink` function to get the download link and opens it in a new window.

Finally, add an `onClick` handler to the digital product medias’ link in the return statement:

```tsx title="src/modules/account/components/digital-products-list/index.tsx"
<a href="#" onClick={(e) => handleDownload(e, media.id)}>
  Download{showMediaCount ? ` ${index + 1}` : ``}
</a>
```

### Test Download Purchased Digital Product Media

To test the latest changes out, open the purchased digital products page and click on the Download link of any media in the table. The media’s download link will open in a new page.

***

## Next Steps

The next steps of this example depend on your use case. This section provides some insight into implementing them.

### Storefront Development

Aside from customizing the Next.js Starter storefront, you can also create a custom storefront. Check out the [Storefront Development](https://docs.medusajs.com/storefront-development) section to learn how to create a storefront.

### Admin Development

In this recipe, you learned how to customize the admin with UI routes. You can also do further customization using widgets. Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin).


---

The best way to deploy Medusa is through Medusa Cloud where you get autoscaling production infrastructure fine tuned for Medusa. Create an account by signing up at cloud.medusajs.com/signup.
