Skip to content

Commit

Permalink
docs: handle product deletion in digital products recipe (#10811)
Browse files Browse the repository at this point in the history
  • Loading branch information
shahednasser authored Jan 3, 2025
1 parent 988931a commit 18b385a
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,19 @@ import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/framework/utils"

export default defineLink(
DigitalProductModule.linkable.digitalProduct,
{
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:

export const orderLinkHighlights = [
Expand Down Expand Up @@ -936,7 +942,7 @@ const DigitalProductsPage = () => {
{digitalProduct.name}
</Table.Cell>
<Table.Cell>
<Link to={`/products/${digitalProduct.product_variant.product_id}`}>
<Link to={`/products/${digitalProduct.product_variant?.product_id}`}>
View Product
</Link>
</Table.Cell>
Expand Down Expand Up @@ -1199,7 +1205,7 @@ return (
onChange={(e) => changeFiles(
index,
{
file: e.target.files[0],
file: e.target.files?.[0],
}
)}
className="mt-2"
Expand Down Expand Up @@ -1279,6 +1285,9 @@ const uploadMediaFiles = async (
}

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

Expand Down Expand Up @@ -1319,7 +1328,11 @@ const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
files: mainFiles,
} = await uploadMediaFiles(MediaType.MAIN) || {}

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

previewMedias?.forEach((media, index) => {
mediaData.push({
Expand Down Expand Up @@ -1480,7 +1493,208 @@ To use this digital product in later steps (such as to create an order), you mus

---

## Step 11: Create Digital Product Fulfillment Module Provider
## 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:

export const retrieveDigitalProductsHighlights = [
["14", "productVariants", "Retrieve the product variants of the deleted product."],
["17", "withDeleted", "Include deleted product variants in the result."],
["20", "graph", "Retrieve the digital products associated with the product variants."],
["21", "DigitalProductVariantLink.entryPoint", "Pass the link as an entry point."],
["28", "digitalProductIds", "Extract the IDs of the digital products."],
["30", "digitalProductIds", "Return the digital product IDs."],
]

```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:

export const deleteDigitalProductsHighlights = [
["15", "softDeleteDigitalProducts", "Soft delete the digital products."],
["27", "restoreDigitalProducts", "Restore the digital products if an error occurs."]
]

```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.

<Note>

Learn more about subscribers in [this documentation](!docs!/learn/fundamentals/events-and-subscribers).

</Note>

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](!docs!/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.

Expand Down Expand Up @@ -1597,7 +1811,7 @@ This is necessary to use the fulfillment provider's shipping option during check

---

## Step 12: Customize Cart Completion
## Step 13: Customize Cart Completion

In this step, you’ll customize the cart completion flow to not only create a Medusa order, but also create a digital product order.

Expand Down Expand Up @@ -1880,7 +2094,7 @@ In a later step, you’ll add an API route to allow customers to view and downlo

---

## Step 13: Fulfill Digital Order Workflow
## 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.

Expand Down Expand Up @@ -2091,9 +2305,7 @@ module.exports = defineConfig({

---

## Step 14: Handle the Digital Product Order Event

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.
## 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.

Expand Down Expand Up @@ -2134,7 +2346,7 @@ To test out the subscriber, place an order with digital products. This triggers

---

## Step 15: Create Store API Routes
## Step 16: Create Store API Routes

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

Expand Down Expand Up @@ -2363,7 +2575,7 @@ You’ll test out these API routes in the next step.

---

## Step 16: Customize Next.js Starter
## Step 17: Customize Next.js Starter

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

Expand Down
2 changes: 1 addition & 1 deletion www/apps/resources/generated/edit-dates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const generatedEditDates = {
"app/nextjs-starter/page.mdx": "2024-12-12T12:31:16.661Z",
"app/recipes/b2b/page.mdx": "2024-10-03T13:07:44.153Z",
"app/recipes/commerce-automation/page.mdx": "2024-10-16T08:52:01.585Z",
"app/recipes/digital-products/examples/standard/page.mdx": "2024-12-13T16:04:34.105Z",
"app/recipes/digital-products/examples/standard/page.mdx": "2025-01-03T14:38:04.333Z",
"app/recipes/digital-products/page.mdx": "2024-10-03T13:07:44.147Z",
"app/recipes/ecommerce/page.mdx": "2024-10-22T11:01:01.218Z",
"app/recipes/integrate-ecommerce-stack/page.mdx": "2024-12-09T13:03:35.846Z",
Expand Down

0 comments on commit 18b385a

Please sign in to comment.