From fbacd15de18cb12a0740429fcd0d75a0f3acb5ce Mon Sep 17 00:00:00 2001 From: Jamal Soueidan Date: Thu, 30 May 2024 18:51:47 +0200 Subject: [PATCH] Refactor schedule update logic and add schedule metafield updates - Implement `updateScheduleLocationsField` to handle schedule location updates - Modify `updateUserMetaobject` to deduplicate locations and include schedule IDs - Introduce `updateScheduleMetafield` activity in schedule update orchestration - Add `UpdateScheduleLocationsFieldMutation` type for GraphQL operations - Remove redundant location aggregation logic from `updateScheduleMetafield` --- .../update/update-user-metaobject.spec.ts | 26 +++- .../customer/update/update-user-metaobject.ts | 14 ++- .../customer/orchestrations/product/update.ts | 13 +- .../update-schedule-locations-field.spec.ts | 118 ++++++++++++++++++ .../update/update-schedule-locations-field.ts | 72 +++++++++++ .../orchestrations/schedule/update.ts | 16 +-- .../update/update-schedule-metafield.spec.ts | 53 +------- .../update/update-schedule-metafield.ts | 13 -- .../user/services/schedule/locations-list.ts | 2 + src/types/admin.generated.d.ts | 9 ++ 10 files changed, 258 insertions(+), 78 deletions(-) create mode 100644 src/functions/customer/orchestrations/product/update/update-schedule-locations-field.spec.ts create mode 100644 src/functions/customer/orchestrations/product/update/update-schedule-locations-field.ts diff --git a/src/functions/customer/orchestrations/customer/update/update-user-metaobject.spec.ts b/src/functions/customer/orchestrations/customer/update/update-user-metaobject.spec.ts index f9a9af13..7d0fd5d2 100644 --- a/src/functions/customer/orchestrations/customer/update/update-user-metaobject.spec.ts +++ b/src/functions/customer/orchestrations/customer/update/update-user-metaobject.spec.ts @@ -37,7 +37,24 @@ describe("CustomerUpdateOrchestration", () => { metafieldId: "1", }); - await createSchedule({ + const schedule = await createSchedule({ + metafieldId: "2", + name: faker.person.lastName(), + customerId, + products: [ + getProductObject({ + locations: [ + getDumbLocationObject({ + location: locationOrigin._id, + metafieldId: locationOrigin.metafieldId, + }), + ], + }), + ], + }); + + const schedule2 = await createSchedule({ + metafieldId: "3", name: faker.person.lastName(), customerId, products: [ @@ -99,6 +116,13 @@ describe("CustomerUpdateOrchestration", () => { key: "locations", value: JSON.stringify(["1"]), }, + { + key: "schedules", + value: JSON.stringify([ + schedule.metafieldId, + schedule2.metafieldId, + ]), + }, { key: "active", value: String(user.active), diff --git a/src/functions/customer/orchestrations/customer/update/update-user-metaobject.ts b/src/functions/customer/orchestrations/customer/update/update-user-metaobject.ts index 901c67b7..c478689c 100644 --- a/src/functions/customer/orchestrations/customer/update/update-user-metaobject.ts +++ b/src/functions/customer/orchestrations/customer/update/update-user-metaobject.ts @@ -10,11 +10,15 @@ export const updateUserMetaobject = async ({ }) => { const user = await CustomerServiceGet({ customerId }); - const schedule = await UserScheduleServiceLocationsList({ + const schedules = await UserScheduleServiceLocationsList({ customerId, }); - const locations = schedule.map((item) => item.locations).flat(); + // save unqiue locations across all schedule in user metafield + const locations = schedules + .map((item) => item.locations) + .flat() + .map((p) => p.metafieldId); const variables = { id: user.userMetaobjectId || "", @@ -41,7 +45,11 @@ export const updateUserMetaobject = async ({ }, { key: "locations", - value: JSON.stringify(locations.map((p) => p.metafieldId)), + value: JSON.stringify([...new Set(locations)]), + }, + { + key: "schedules", + value: JSON.stringify(schedules.map((p) => p.metafieldId)), }, { key: "active", diff --git a/src/functions/customer/orchestrations/product/update.ts b/src/functions/customer/orchestrations/product/update.ts index 91aab373..059ed61f 100644 --- a/src/functions/customer/orchestrations/product/update.ts +++ b/src/functions/customer/orchestrations/product/update.ts @@ -8,6 +8,10 @@ import { } from "../customer/update/update-user-metaobject"; import { updatePrice, updatePriceName } from "./update/update-price"; import { updateProduct, updateProductName } from "./update/update-product"; +import { + updateScheduleLocationsField, + updateScheduleLocationsFieldName, +} from "./update/update-schedule-locations-field"; df.app.activity(updateProductName, { handler: updateProduct }); df.app.activity(updatePriceName, { handler: updatePrice }); @@ -35,7 +39,14 @@ const orchestrator: df.OrchestrationHandler = function* ( activityType(input) ); - return { productUpdated, priceUpdated, userField }; + const scheduleLocationsField: Awaited< + ReturnType + > = yield context.df.callActivity( + updateScheduleLocationsFieldName, + activityType(input) + ); + + return { productUpdated, priceUpdated, userField, scheduleLocationsField }; }; df.app.orchestration("updateProductShopify", orchestrator); diff --git a/src/functions/customer/orchestrations/product/update/update-schedule-locations-field.spec.ts b/src/functions/customer/orchestrations/product/update/update-schedule-locations-field.spec.ts new file mode 100644 index 00000000..7595bc35 --- /dev/null +++ b/src/functions/customer/orchestrations/product/update/update-schedule-locations-field.spec.ts @@ -0,0 +1,118 @@ +import { ensureType } from "~/library/jest/helpers/mock"; +import { createSchedule } from "~/library/jest/helpers/schedule"; +import { shopifyAdmin } from "~/library/shopify"; +import { + UpdateScheduleLocationsFieldMutation, + UpdateScheduleLocationsFieldMutationVariables, +} from "~/types/admin.generated"; + +import { + createLocation, + getDumbLocationObject, +} from "~/library/jest/helpers/location"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { + UPDATE_SCHEDULE_LOCATIONS_FIELD, + updateScheduleLocationsField, +} from "./update-schedule-locations-field"; + +require("~/library/jest/mongoose/mongodb.jest"); + +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ + request: jest.fn(), + }), +})); + +const mockRequest = shopifyAdmin().request as jest.Mock; + +describe("CustomerProductUpdateOrchestration", () => { + beforeAll(async () => { + jest.clearAllMocks(); + }); + + it("updateScheduleLocationsField", async () => { + const location1 = await createLocation({ + customerId: 123, + metafieldId: "asd", + }); + + const location2 = await createLocation({ + customerId: 123, + metafieldId: "2", + }); + + const product1 = getProductObject({ + locations: [ + getDumbLocationObject({ + location: location1._id, + metafieldId: location1.metafieldId, + }), + getDumbLocationObject({ + location: location2._id, + metafieldId: location2.metafieldId, + }), + ], + }); + + const product2 = getProductObject({ + locations: [ + getDumbLocationObject({ + location: location1._id, + metafieldId: location1.metafieldId, + }), + ], + }); + + const newSchedule = await createSchedule({ + metafieldId: "1", + name: "ANOTHER CUSTOMER", + customerId: 7, + products: [product1, product2], + }); + + mockRequest.mockResolvedValueOnce({ + data: ensureType({ + metaobjectUpdate: { + metaobject: { + fields: [ + { + value: JSON.stringify([ + location1.metafieldId, + location2.metafieldId, + ]), + key: "locations", + }, + ], + }, + }, + }), + }); + + await updateScheduleLocationsField({ + productId: product1.productId, + customerId: newSchedule.customerId, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + + expect(mockRequest).toHaveBeenNthCalledWith( + 1, + UPDATE_SCHEDULE_LOCATIONS_FIELD, + { + variables: ensureType({ + id: newSchedule.metafieldId || "", + fields: [ + { + value: JSON.stringify([ + location1.metafieldId, + location2.metafieldId, + ]), + key: "locations", + }, + ], + }), + } + ); + }); +}); diff --git a/src/functions/customer/orchestrations/product/update/update-schedule-locations-field.ts b/src/functions/customer/orchestrations/product/update/update-schedule-locations-field.ts new file mode 100644 index 00000000..9e1cca4e --- /dev/null +++ b/src/functions/customer/orchestrations/product/update/update-schedule-locations-field.ts @@ -0,0 +1,72 @@ +import { ScheduleModel } from "~/functions/schedule"; +import { shopifyAdmin } from "~/library/shopify"; + +export const updateScheduleLocationsFieldName = "updateScheduleLocationsField"; +export const updateScheduleLocationsField = async ({ + productId, + customerId, +}: { + productId: number; + customerId: number; +}) => { + const schedule = await ScheduleModel.findOne({ + customerId, + products: { + $elemMatch: { + productId, + }, + }, + }); + + if (!schedule?.metafieldId) { + throw new Error( + `Failed to update schedule locations field for productId ${productId}` + ); + } + + // save unique locations for this schedule metafield that are found in the current schedule model. + const locations = schedule.products.reduce((locations, product) => { + product.locations.forEach((location) => { + if (location.metafieldId && !locations.includes(location.metafieldId)) { + locations.push(location.metafieldId); + } + }); + return locations; + }, [] as string[]); + + const { data } = await shopifyAdmin().request( + UPDATE_SCHEDULE_LOCATIONS_FIELD, + { + variables: { + id: schedule.metafieldId, + fields: [ + { + key: "locations", + value: JSON.stringify(locations), + }, + ], + }, + } + ); + + if (!data?.metaobjectUpdate?.metaobject) { + throw new Error( + `Failed to update schedule locations field for schedule ${schedule._id}` + ); + } + + return data?.metaobjectUpdate.metaobject; +}; + +export const UPDATE_SCHEDULE_LOCATIONS_FIELD = `#graphql + mutation UpdateScheduleLocationsField($id: ID!, $fields: [MetaobjectFieldInput!]!) { + metaobjectUpdate(id: $id, metaobject: {fields: $fields}) { + metaobject { + fields { + value + key + } + } + } + } +` as const; diff --git a/src/functions/customer/orchestrations/schedule/update.ts b/src/functions/customer/orchestrations/schedule/update.ts index 1a7b2d6f..cde1dc77 100644 --- a/src/functions/customer/orchestrations/schedule/update.ts +++ b/src/functions/customer/orchestrations/schedule/update.ts @@ -4,12 +4,12 @@ import { OrchestrationContext } from "durable-functions"; import { activityType } from "~/library/orchestration"; import { StringOrObjectIdType } from "~/library/zod"; import { - createScheduleMetafield, - createScheduleMetafieldName, -} from "./create/create-schedule-metafield"; + updateScheduleMetafield, + updateScheduleMetafieldName, +} from "./update/update-schedule-metafield"; -df.app.activity(createScheduleMetafieldName, { - handler: createScheduleMetafield, +df.app.activity(updateScheduleMetafieldName, { + handler: updateScheduleMetafield, }); const orchestrator: df.OrchestrationHandler = function* ( @@ -17,10 +17,10 @@ const orchestrator: df.OrchestrationHandler = function* ( ) { const input = context.df.getInput() as Input; - const metafield: Awaited> = + const metafield: Awaited> = yield context.df.callActivity( - createScheduleMetafieldName, - activityType(input) + updateScheduleMetafieldName, + activityType(input) ); return { metafield }; diff --git a/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.spec.ts b/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.spec.ts index e2006e68..60454f04 100644 --- a/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.spec.ts +++ b/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.spec.ts @@ -6,11 +6,6 @@ import { UpdateScheduleMetaobjectMutationVariables, } from "~/types/admin.generated"; -import { - createLocation, - getDumbLocationObject, -} from "~/library/jest/helpers/location"; -import { getProductObject } from "~/library/jest/helpers/product"; import { UPDATE_SCHEDULE_METAOBJECT, updateScheduleMetafield, @@ -32,43 +27,11 @@ describe("CustomerScheduleUpdateOrchestration", () => { }); it("updateScheduleMetafield", async () => { - const location1 = await createLocation({ - customerId: 123, - metafieldId: "asd", - }); - - const location2 = await createLocation({ - customerId: 123, - metafieldId: "2", - }); - - const product1 = getProductObject({ - locations: [ - getDumbLocationObject({ - location: location1._id, - metafieldId: location1.metafieldId, - }), - getDumbLocationObject({ - location: location2._id, - metafieldId: location2.metafieldId, - }), - ], - }); - - const product2 = getProductObject({ - locations: [ - getDumbLocationObject({ - location: location1._id, - metafieldId: location1.metafieldId, - }), - ], - }); - const newSchedule = await createSchedule({ metafieldId: "1", name: "ANOTHER CUSTOMER", customerId: 7, - products: [product1, product2], + products: [], }); mockRequest.mockResolvedValueOnce({ @@ -84,13 +47,6 @@ describe("CustomerScheduleUpdateOrchestration", () => { value: JSON.stringify(newSchedule.slots), key: "slots", }, - { - value: JSON.stringify([ - location1.metafieldId, - location2.metafieldId, - ]), - key: "locations", - }, ], }, }, @@ -126,13 +82,6 @@ describe("CustomerScheduleUpdateOrchestration", () => { ]), key: "slots", }, - { - value: JSON.stringify([ - location1.metafieldId, - location2.metafieldId, - ]), - key: "locations", - }, ], }), }); diff --git a/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.ts b/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.ts index c068877e..3e6459d7 100644 --- a/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.ts +++ b/src/functions/customer/orchestrations/schedule/update/update-schedule-metafield.ts @@ -18,15 +18,6 @@ export const updateScheduleMetafield = async ({ ); } - const locations = schedule.products.reduce((locations, product) => { - product.locations.forEach((location) => { - if (location.metafieldId && !locations.includes(location.metafieldId)) { - locations.push(location.metafieldId); - } - }); - return locations; - }, [] as string[]); - const { data } = await shopifyAdmin().request(UPDATE_SCHEDULE_METAOBJECT, { variables: { id: schedule.metafieldId, @@ -39,10 +30,6 @@ export const updateScheduleMetafield = async ({ key: "slots", value: JSON.stringify(schedule.slots), }, - { - key: "locations", - value: JSON.stringify(locations), - }, ], }, }); diff --git a/src/functions/user/services/schedule/locations-list.ts b/src/functions/user/services/schedule/locations-list.ts index 26636973..213c19a4 100644 --- a/src/functions/user/services/schedule/locations-list.ts +++ b/src/functions/user/services/schedule/locations-list.ts @@ -39,6 +39,7 @@ export const UserScheduleServiceLocationsList = async ({ locations: { $addToSet: "$products.locations" }, name: { $first: "$name" }, slots: { $first: "$slots" }, + metafieldId: { $first: "$metafieldId" }, createdAt: { $first: "$createdAt" }, updatedAt: { $first: "$updatedAt" }, }, @@ -52,6 +53,7 @@ export const UserScheduleServiceLocationsList = async ({ locations: { $addToSet: "$locations" }, createdAt: { $first: "$createdAt" }, updatedAt: { $first: "$updatedAt" }, + metafieldId: { $first: "$metafieldId" }, }, }, { diff --git a/src/types/admin.generated.d.ts b/src/types/admin.generated.d.ts index f0942b7d..35de31e8 100644 --- a/src/types/admin.generated.d.ts +++ b/src/types/admin.generated.d.ts @@ -139,6 +139,14 @@ export type ProductUpdateMutation = { productUpdate?: AdminTypes.Maybe<{ product & { variants: { nodes: Array> }, default?: AdminTypes.Maybe>, active?: AdminTypes.Maybe>, user?: AdminTypes.Maybe>, hideFromCombine?: AdminTypes.Maybe>, hideFromProfile?: AdminTypes.Maybe>, parentId?: AdminTypes.Maybe>, scheduleId?: AdminTypes.Maybe>, locations?: AdminTypes.Maybe>, bookingPeriodValue?: AdminTypes.Maybe>, bookingPeriodUnit?: AdminTypes.Maybe>, noticePeriodValue?: AdminTypes.Maybe>, noticePeriodUnit?: AdminTypes.Maybe>, duration?: AdminTypes.Maybe>, breaktime?: AdminTypes.Maybe> } )> }> }; +export type UpdateScheduleLocationsFieldMutationVariables = AdminTypes.Exact<{ + id: AdminTypes.Scalars['ID']['input']; + fields: Array | AdminTypes.MetaobjectFieldInput; +}>; + + +export type UpdateScheduleLocationsFieldMutation = { metaobjectUpdate?: AdminTypes.Maybe<{ metaobject?: AdminTypes.Maybe<{ fields: Array> }> }> }; + export type CreateScheduleMetaobjectMutationVariables = AdminTypes.Exact<{ handle: AdminTypes.Scalars['String']['input']; fields?: AdminTypes.InputMaybe | AdminTypes.MetaobjectFieldInput>; @@ -244,6 +252,7 @@ interface GeneratedMutationTypes { "#graphql\n mutation productDestroy($productId: ID!) {\n productDelete(input: {id: $productId}) {\n deletedProductId\n }\n }\n": {return: ProductDestroyMutation, variables: ProductDestroyMutationVariables}, "#graphql\n mutation productPricepdate($id: ID!, $variants: [ProductVariantsBulkInput!] = {}) {\n productVariantsBulkUpdate(\n productId: $id,\n variants: $variants\n ) {\n product {\n id\n variants(first: 1) {\n nodes {\n id\n compareAtPrice\n price\n }\n }\n }\n }\n }\n": {return: ProductPricepdateMutation, variables: ProductPricepdateMutationVariables}, "#graphql\n #graphql\n fragment ProductFragment on Product {\n id\n handle\n tags\n title\n variants(first: 1) {\n nodes {\n id\n compareAtPrice\n price\n }\n }\n default: metafield(key: \"default\", namespace: \"system\") {\n id\n value\n }\n active: metafield(key: \"active\", namespace: \"system\") {\n id\n value\n }\n user: metafield(key: \"user\", namespace: \"booking\") {\n id\n value\n }\n hideFromCombine: metafield(key: \"hide_from_combine\", namespace: \"booking\") {\n id\n value\n }\n hideFromProfile: metafield(key: \"hide_from_profile\", namespace: \"booking\") {\n id\n value\n }\n parentId: metafield(key: \"parentId\", namespace: \"booking\") {\n id\n value\n }\n scheduleId: metafield(key: \"scheduleId\", namespace: \"booking\") {\n id\n value\n }\n locations: metafield(key: \"locations\", namespace: \"booking\") {\n id\n value\n }\n bookingPeriodValue: metafield(key: \"booking_period_value\", namespace: \"booking\") {\n id\n value\n }\n bookingPeriodUnit: metafield(key: \"booking_period_unit\", namespace: \"booking\") {\n id\n value\n }\n noticePeriodValue: metafield(key: \"notice_period_value\", namespace: \"booking\") {\n id\n value\n }\n noticePeriodUnit: metafield(key: \"notice_period_unit\", namespace: \"booking\") {\n id\n value\n }\n duration: metafield(key: \"duration\", namespace: \"booking\") {\n id\n value\n }\n breaktime: metafield(key: \"breaktime\", namespace: \"booking\") {\n id\n value\n }\n }\n\n mutation ProductUpdate($id: ID, $metafields: [MetafieldInput!], $tags: [String!], $title: String, $descriptionHtml: String) {\n productUpdate(input: {id: $id, metafields: $metafields, tags: $tags, title: $title, descriptionHtml: $descriptionHtml}) {\n product {\n ...ProductFragment\n }\n }\n }\n": {return: ProductUpdateMutation, variables: ProductUpdateMutationVariables}, + "#graphql\n mutation UpdateScheduleLocationsField($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateScheduleLocationsFieldMutation, variables: UpdateScheduleLocationsFieldMutationVariables}, "#graphql\n mutation CreateScheduleMetaobject($handle: String!, $fields: [MetaobjectFieldInput!]) {\n metaobjectCreate(\n metaobject: {type: \"schedule\", fields: $fields, handle: $handle, capabilities: {publishable: {status: ACTIVE}}}\n ) {\n metaobject {\n id\n type\n fields {\n value\n key\n }\n }\n }\n }\n": {return: CreateScheduleMetaobjectMutation, variables: CreateScheduleMetaobjectMutationVariables}, "#graphql\n mutation UpdateScheduleMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateScheduleMetaobjectMutation, variables: UpdateScheduleMetaobjectMutationVariables}, "#graphql\n mutation UpdateLocationMetaobject($id: ID!, $fields: [MetaobjectFieldInput!]!) {\n metaobjectUpdate(id: $id, metaobject: {fields: $fields}) {\n metaobject {\n fields {\n value\n key\n }\n }\n }\n }\n": {return: UpdateLocationMetaobjectMutation, variables: UpdateLocationMetaobjectMutationVariables},