From 18b385aff795eaaedc961c28826bbe82d4f79972 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 3 Jan 2025 17:02:56 +0200 Subject: [PATCH] docs: handle product deletion in digital products recipe (#10811) --- .../examples/standard/page.mdx | 236 +++++++++++++++++- www/apps/resources/generated/edit-dates.mjs | 2 +- 2 files changed, 225 insertions(+), 13 deletions(-) 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",