docs: handle product deletion in digital products recipe (#10811)

This commit is contained in:
Shahed Nasser
2025-01-03 17:02:56 +02:00
committed by GitHub
parent 988931a551
commit 18b385aff7
2 changed files with 225 additions and 13 deletions

View File

@@ -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 Modules `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 = [
@@ -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>
@@ -1199,7 +1205,7 @@ return (
onChange={(e) => changeFiles(
index,
{
file: e.target.files[0],
file: e.target.files?.[0],
}
)}
className="mt-2"
@@ -1279,6 +1285,9 @@ const uploadMediaFiles = async (
}
mediaWithFiles.forEach((media) => {
if (!media.file) {
return
}
formData.append("files", media.file)
})
@@ -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({
@@ -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.
@@ -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, youll customize the cart completion flow to not only create a Medusa order, but also create a digital product order.
@@ -1880,7 +2094,7 @@ In a later step, youll 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.
@@ -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.
@@ -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, youll create three store API routes:
@@ -2363,7 +2575,7 @@ Youll test out these API routes in the next step.
---
## Step 16: Customize Next.js Starter
## Step 17: Customize Next.js Starter
In this section, youll customize the [Next.js Starter storefront](../../../../nextjs-starter/page.mdx) to:

View File

@@ -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",