From 57077406f9ce5d3bc1008b643a055f294de838eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:00:41 +0200 Subject: [PATCH] fix(fulfillment): don't cascade shipping option delete to shipping option type (#13280) **What** - don't cascade delete shipping option type when shipping option is deleted since types can be shared between options - prevent shipping option type deletion if there are options associated with it --- .changeset/light-cats-lie.md | 6 + .../workflows/delete-shipping-option-types.ts | 51 ++++- .../fulfillment-module-service/index.spec.ts | 2 +- .../.snapshot-medusa-fulfillment.json | 195 ++++++++++++------ .../src/migrations/Migration20250822130931.ts | 9 + .../fulfillment/src/models/shipping-option.ts | 2 +- 6 files changed, 194 insertions(+), 71 deletions(-) create mode 100644 .changeset/light-cats-lie.md create mode 100644 packages/modules/fulfillment/src/migrations/Migration20250822130931.ts diff --git a/.changeset/light-cats-lie.md b/.changeset/light-cats-lie.md new file mode 100644 index 0000000000..851e8a1deb --- /dev/null +++ b/.changeset/light-cats-lie.md @@ -0,0 +1,6 @@ +--- +"@medusajs/fulfillment": patch +"@medusajs/core-flows": patch +--- + +fix(core-flows, fulfillment): don't cascade delete shipping option type when shipping option is deleted diff --git a/packages/core/core-flows/src/shipping-options/workflows/delete-shipping-option-types.ts b/packages/core/core-flows/src/shipping-options/workflows/delete-shipping-option-types.ts index 57f7114ff4..2fbc96c337 100644 --- a/packages/core/core-flows/src/shipping-options/workflows/delete-shipping-option-types.ts +++ b/packages/core/core-flows/src/shipping-options/workflows/delete-shipping-option-types.ts @@ -1,4 +1,8 @@ -import { Modules, ShippingOptionTypeWorkflowEvents } from "@medusajs/framework/utils" +import { + MedusaError, + Modules, + ShippingOptionTypeWorkflowEvents, +} from "@medusajs/framework/utils" import { createHook, createWorkflow, @@ -7,9 +11,25 @@ import { WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" +import { createStep } from "@medusajs/framework/workflows-sdk" import { emitEventStep } from "../../common/steps/emit-event" import { removeRemoteLinkStep } from "../../common/steps/remove-remote-links" import { deleteShippingOptionTypesStep } from "../steps" +import { useQueryGraphStep } from "../../common" + +const validateDeleteShippingOptionTypesStep = createStep( + "validate-delete-shipping-option-types", + (input: { shippingOptions: { id: string }[] }) => { + const shippingOptions = input.shippingOptions + + if (shippingOptions.length > 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Cannot delete shipping option type because some shipping options are using it." + ) + } + } +) /** * The data to delete one or more shipping option types. @@ -21,7 +41,8 @@ export type DeleteShippingOptionTypesWorkflowInput = { ids: string[] } -export const deleteShippingOptionTypesWorkflowId = "delete-shipping-option-types" +export const deleteShippingOptionTypesWorkflowId = + "delete-shipping-option-types" /** * This workflow deletes one or more shipping-option types. It's used by the * [Delete Shipping Option Types Admin API Route](TODO HERE). @@ -48,11 +69,31 @@ export const deleteShippingOptionTypesWorkflowId = "delete-shipping-option-types export const deleteShippingOptionTypesWorkflow = createWorkflow( deleteShippingOptionTypesWorkflowId, (input: WorkflowData) => { - const deletedShippingOptionTypes = deleteShippingOptionTypesStep(input.ids) - const shippingOptionTypesDeleted = createHook("shippingOptionTypesDeleted", { - ids: input.ids, + const shippingOptionsQuery = useQueryGraphStep({ + entity: "shipping_option", + filters: { shipping_option_type_id: input.ids }, + pagination: { take: 1 }, + fields: ["id"], + }).config({ name: "get-shipping-options" }) + + const shippingOptions = transform( + { shippingOptionsQuery }, + ({ shippingOptionsQuery }) => + shippingOptionsQuery.data as { id: string }[] + ) + + validateDeleteShippingOptionTypesStep({ + shippingOptions, }) + const deletedShippingOptionTypes = deleteShippingOptionTypesStep(input.ids) + const shippingOptionTypesDeleted = createHook( + "shippingOptionTypesDeleted", + { + ids: input.ids, + } + ) + const typeIdEvents = transform({ input }, ({ input }) => { return input.ids?.map((id) => { return { id } diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/index.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/index.spec.ts index 29f1444f54..72ff7c738a 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/index.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/index.spec.ts @@ -74,7 +74,7 @@ function expectSoftDeleted( let shippingOption = serviceZone.shipping_options[0] expect(!!shippingOption.deleted_at).toEqual(softDeleted) expect(!!shippingOption.shipping_profile.deleted_at).toEqual(false) - expect(!!shippingOption.type.deleted_at).toEqual(softDeleted) + expect(!!shippingOption.type.deleted_at).toEqual(false) // do not cascade delete shipping option type since it is shared between shipping options expect(shippingOption.fulfillments).toHaveLength(1) expect(shippingOption.rules).toHaveLength(1) diff --git a/packages/modules/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json b/packages/modules/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json index 9e67680c91..eb7e7fec8f 100644 --- a/packages/modules/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json +++ b/packages/modules/fulfillment/src/migrations/.snapshot-medusa-fulfillment.json @@ -1,5 +1,7 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ { @@ -159,7 +161,9 @@ }, { "keyName": "fulfillment_address_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -238,7 +242,9 @@ }, { "keyName": "fulfillment_provider_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -343,7 +349,9 @@ }, { "keyName": "fulfillment_set_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -457,7 +465,9 @@ }, { "keyName": "service_zone_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -468,9 +478,13 @@ "foreignKeys": { "service_zone_fulfillment_set_id_foreign": { "constraintName": "service_zone_fulfillment_set_id_foreign", - "columnNames": ["fulfillment_set_id"], + "columnNames": [ + "fulfillment_set_id" + ], "localTableName": "public.service_zone", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.fulfillment_set", "deleteRule": "cascade", "updateRule": "cascade" @@ -497,7 +511,12 @@ "primary": false, "nullable": false, "default": "'country'", - "enumItems": ["country", "province", "city", "zip"], + "enumItems": [ + "country", + "province", + "city", + "zip" + ], "mappedType": "enum" }, "country_code": { @@ -637,7 +656,9 @@ }, { "keyName": "geo_zone_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -648,9 +669,13 @@ "foreignKeys": { "geo_zone_service_zone_id_foreign": { "constraintName": "geo_zone_service_zone_id_foreign", - "columnNames": ["service_zone_id"], + "columnNames": [ + "service_zone_id" + ], "localTableName": "public.geo_zone", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.service_zone", "deleteRule": "cascade", "updateRule": "cascade" @@ -743,7 +768,9 @@ }, { "keyName": "shipping_option_type_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -848,7 +875,9 @@ }, { "keyName": "shipping_profile_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -887,7 +916,10 @@ "primary": false, "nullable": false, "default": "'flat'", - "enumItems": ["calculated", "flat"], + "enumItems": [ + "calculated", + "flat" + ], "mappedType": "enum" }, "data": { @@ -980,14 +1012,6 @@ "name": "shipping_option", "schema": "public", "indexes": [ - { - "columnNames": ["shipping_option_type_id"], - "composite": false, - "keyName": "shipping_option_shipping_option_type_id_unique", - "constraint": true, - "primary": false, - "unique": true - }, { "keyName": "IDX_shipping_option_service_zone_id", "columnNames": [], @@ -1026,7 +1050,9 @@ }, { "keyName": "shipping_option_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -1037,36 +1063,52 @@ "foreignKeys": { "shipping_option_service_zone_id_foreign": { "constraintName": "shipping_option_service_zone_id_foreign", - "columnNames": ["service_zone_id"], + "columnNames": [ + "service_zone_id" + ], "localTableName": "public.shipping_option", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.service_zone", "deleteRule": "cascade", "updateRule": "cascade" }, "shipping_option_shipping_profile_id_foreign": { "constraintName": "shipping_option_shipping_profile_id_foreign", - "columnNames": ["shipping_profile_id"], + "columnNames": [ + "shipping_profile_id" + ], "localTableName": "public.shipping_option", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.shipping_profile", "deleteRule": "set null", "updateRule": "cascade" }, "shipping_option_provider_id_foreign": { "constraintName": "shipping_option_provider_id_foreign", - "columnNames": ["provider_id"], + "columnNames": [ + "provider_id" + ], "localTableName": "public.shipping_option", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.fulfillment_provider", "deleteRule": "set null", "updateRule": "cascade" }, "shipping_option_shipping_option_type_id_foreign": { "constraintName": "shipping_option_shipping_option_type_id_foreign", - "columnNames": ["shipping_option_type_id"], + "columnNames": [ + "shipping_option_type_id" + ], "localTableName": "public.shipping_option", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.shipping_option_type", "updateRule": "cascade" } @@ -1100,7 +1142,16 @@ "autoincrement": false, "primary": false, "nullable": false, - "enumItems": ["in", "eq", "ne", "gt", "gte", "lt", "lte", "nin"], + "enumItems": [ + "in", + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", + "nin" + ], "mappedType": "enum" }, "value": { @@ -1177,7 +1228,9 @@ }, { "keyName": "shipping_option_rule_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -1188,9 +1241,13 @@ "foreignKeys": { "shipping_option_rule_shipping_option_id_foreign": { "constraintName": "shipping_option_rule_shipping_option_id_foreign", - "columnNames": ["shipping_option_id"], + "columnNames": [ + "shipping_option_id" + ], "localTableName": "public.shipping_option_rule", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.shipping_option", "deleteRule": "cascade", "updateRule": "cascade" @@ -1367,22 +1424,6 @@ "name": "fulfillment", "schema": "public", "indexes": [ - { - "columnNames": ["provider_id"], - "composite": false, - "keyName": "fulfillment_provider_id_unique", - "constraint": true, - "primary": false, - "unique": true - }, - { - "columnNames": ["delivery_address_id"], - "composite": false, - "keyName": "fulfillment_delivery_address_id_unique", - "constraint": true, - "primary": false, - "unique": true - }, { "keyName": "IDX_fulfillment_shipping_option_id", "columnNames": [], @@ -1412,7 +1453,9 @@ }, { "keyName": "fulfillment_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -1423,27 +1466,39 @@ "foreignKeys": { "fulfillment_provider_id_foreign": { "constraintName": "fulfillment_provider_id_foreign", - "columnNames": ["provider_id"], + "columnNames": [ + "provider_id" + ], "localTableName": "public.fulfillment", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.fulfillment_provider", "deleteRule": "set null", "updateRule": "cascade" }, "fulfillment_shipping_option_id_foreign": { "constraintName": "fulfillment_shipping_option_id_foreign", - "columnNames": ["shipping_option_id"], + "columnNames": [ + "shipping_option_id" + ], "localTableName": "public.fulfillment", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.shipping_option", "deleteRule": "set null", "updateRule": "cascade" }, "fulfillment_delivery_address_id_foreign": { "constraintName": "fulfillment_delivery_address_id_foreign", - "columnNames": ["delivery_address_id"], + "columnNames": [ + "delivery_address_id" + ], "localTableName": "public.fulfillment", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.fulfillment_address", "deleteRule": "set null", "updateRule": "cascade" @@ -1554,7 +1609,9 @@ }, { "keyName": "fulfillment_label_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -1565,9 +1622,13 @@ "foreignKeys": { "fulfillment_label_fulfillment_id_foreign": { "constraintName": "fulfillment_label_fulfillment_id_foreign", - "columnNames": ["fulfillment_id"], + "columnNames": [ + "fulfillment_id" + ], "localTableName": "public.fulfillment_label", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.fulfillment", "deleteRule": "cascade", "updateRule": "cascade" @@ -1732,7 +1793,9 @@ }, { "keyName": "fulfillment_item_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "constraint": true, "primary": true, @@ -1743,9 +1806,13 @@ "foreignKeys": { "fulfillment_item_fulfillment_id_foreign": { "constraintName": "fulfillment_item_fulfillment_id_foreign", - "columnNames": ["fulfillment_id"], + "columnNames": [ + "fulfillment_id" + ], "localTableName": "public.fulfillment_item", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.fulfillment", "deleteRule": "cascade", "updateRule": "cascade" diff --git a/packages/modules/fulfillment/src/migrations/Migration20250822130931.ts b/packages/modules/fulfillment/src/migrations/Migration20250822130931.ts new file mode 100644 index 0000000000..b657918c7c --- /dev/null +++ b/packages/modules/fulfillment/src/migrations/Migration20250822130931.ts @@ -0,0 +1,9 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20250822130931 extends Migration { + override async up(): Promise { + 'alter table if exists "shipping_option" drop constraint if exists "shipping_option_shipping_option_type_id_foreign", add constraint "shipping_option_shipping_option_type_id_foreign" foreign key ("shipping_option_type_id") references "shipping_option_type" ("id") on update cascade;' + } + + override async down(): Promise {} +} diff --git a/packages/modules/fulfillment/src/models/shipping-option.ts b/packages/modules/fulfillment/src/models/shipping-option.ts index 0327cbb000..9a1ee4ff3b 100644 --- a/packages/modules/fulfillment/src/models/shipping-option.ts +++ b/packages/modules/fulfillment/src/models/shipping-option.ts @@ -38,5 +38,5 @@ export const ShippingOption = model }), }) .cascades({ - delete: ["rules", "type"], + delete: ["rules"], })