diff --git a/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx b/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx
index c62ea1d47c..3bf0f5ada5 100644
--- a/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx
+++ b/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx
@@ -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}
-
+
View Product
@@ -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) => {
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.
+
+
+
+Learn more about subscribers in [this documentation](!docs!/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](!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:
diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs
index d513ad1627..41e463f3ea 100644
--- a/www/apps/resources/generated/edit-dates.mjs
+++ b/www/apps/resources/generated/edit-dates.mjs
@@ -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",