docs: handle product deletion in digital products recipe (#10811)
This commit is contained in:
@@ -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 = [
|
||||
@@ -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, you’ll 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, 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.
|
||||
|
||||
@@ -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, you’ll create three store API routes:
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user