diff --git a/src/functions/customer-product.function.ts b/src/functions/customer-product.function.ts index 4093f7e5..14507690 100644 --- a/src/functions/customer-product.function.ts +++ b/src/functions/customer-product.function.ts @@ -86,4 +86,5 @@ app.http("customerProductDestroy", { authLevel: "anonymous", route: "customer/{customerId?}/product/{productId?}", handler: CustomerProductControllerDestroy, + extraInputs: [df.input.durableClient()], }); diff --git a/src/functions/customer-upload.orchestrators.ts b/src/functions/customer-upload.orchestrators.ts index 11e80ce3..1735af41 100644 --- a/src/functions/customer-upload.orchestrators.ts +++ b/src/functions/customer-upload.orchestrators.ts @@ -82,7 +82,7 @@ df.app.orchestration("upload", function* (context: OrchestrationContext) { yield context.df.callActivity( updateArticleName, activityType({ - user, + customerId: user.customerId, }) ); } diff --git a/src/functions/customer/controllers/product/add.spec.ts b/src/functions/customer/controllers/product/add.spec.ts index cb995bbf..22e729eb 100644 --- a/src/functions/customer/controllers/product/add.spec.ts +++ b/src/functions/customer/controllers/product/add.spec.ts @@ -23,8 +23,8 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("../../orchestrations/product/update", () => ({ - CustomerProductUpdateOrchestration: () => ({ +jest.mock("../../orchestrations/product/add", () => ({ + CustomerProductAddOrchestration: () => ({ request: jest.fn(), }), })); diff --git a/src/functions/customer/controllers/product/add.ts b/src/functions/customer/controllers/product/add.ts index 58847e6c..ce2a7f5e 100644 --- a/src/functions/customer/controllers/product/add.ts +++ b/src/functions/customer/controllers/product/add.ts @@ -4,7 +4,7 @@ import { HttpRequest, InvocationContext } from "@azure/functions"; import { ScheduleProductZodSchema } from "~/functions/schedule"; import { _ } from "~/library/handler"; import { GidFormat, StringOrObjectId } from "~/library/zod"; -import { CustomerProductUpdateOrchestration } from "../../orchestrations/product/update"; +import { CustomerProductAddOrchestration } from "../../orchestrations/product/add"; import { CustomerProductServiceAdd } from "../../services/product/add"; export type CustomerProductControllerAddRequest = { @@ -53,7 +53,7 @@ export const CustomerProductControllerAdd = _( validateBody ); - await CustomerProductUpdateOrchestration( + await CustomerProductAddOrchestration( { productId: product.productId, customerId: validateQuery.customerId }, context ); diff --git a/src/functions/customer/controllers/product/destroy.spec.ts b/src/functions/customer/controllers/product/destroy.spec.ts index 66bc60bd..25339519 100644 --- a/src/functions/customer/controllers/product/destroy.spec.ts +++ b/src/functions/customer/controllers/product/destroy.spec.ts @@ -9,7 +9,6 @@ import { import { getProductObject } from "~/library/jest/helpers/product"; import { createScheduleWithProducts } from "~/library/jest/helpers/schedule"; -import { shopifyAdmin } from "~/library/shopify"; import { CustomerProductControllerDestroy, CustomerProductControllerDestroyRequest, @@ -18,14 +17,12 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("~/library/shopify", () => ({ - shopifyAdmin: jest.fn().mockReturnValue({ +jest.mock("../../orchestrations/product/destroy", () => ({ + CustomerProductDestroyOrchestration: () => ({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin().request as jest.Mock; - describe("CustomerProductControllerDestroy", () => { let context: InvocationContext; let request: HttpRequest; @@ -53,21 +50,12 @@ describe("CustomerProductControllerDestroy", () => { products: [product], }); - mockRequest.mockResolvedValueOnce({ - data: { - productDelete: { - deletedProductId: { - id: "123", - }, - }, - }, - }); - request = await createHttpRequest({ query: { customerId: newSchedule.customerId, productId: product.productId, }, + context, }); const res: HttpSuccessResponse = diff --git a/src/functions/customer/controllers/product/destroy.ts b/src/functions/customer/controllers/product/destroy.ts index 99af4edb..b904bbda 100644 --- a/src/functions/customer/controllers/product/destroy.ts +++ b/src/functions/customer/controllers/product/destroy.ts @@ -1,13 +1,17 @@ +import { InvocationContext } from "@azure/functions"; import { z } from "zod"; import { ScheduleProductZodSchema, ScheduleZodSchema, } from "~/functions/schedule/schedule.types"; import { _ } from "~/library/handler"; +import { CustomerProductDestroyOrchestration } from "../../orchestrations/product/destroy"; import { CustomerProductServiceDestroy } from "../../services/product/destroy"; +import { CustomerProductServiceGet } from "../../services/product/get"; export type CustomerProductControllerDestroyRequest = { query: z.infer; + context: InvocationContext; }; const CustomerProductControllerDestroyQuerySchema = z.object({ @@ -20,9 +24,15 @@ export type CustomerProductControllerDestroyResponse = Awaited< >; export const CustomerProductControllerDestroy = _( - ({ query }: CustomerProductControllerDestroyRequest) => { + async ({ query, context }: CustomerProductControllerDestroyRequest) => { const validateQuery = CustomerProductControllerDestroyQuerySchema.parse(query); + + // we must find the product before throwing it to durable function, since destroy may delete the product before + // we can get hold of the product.options in the durable activate functions. + const product = await CustomerProductServiceGet(validateQuery); + await CustomerProductDestroyOrchestration({ product }, context); + return CustomerProductServiceDestroy(validateQuery); } ); diff --git a/src/functions/customer/controllers/product/update.spec.ts b/src/functions/customer/controllers/product/update.spec.ts index 0eade418..281db6da 100644 --- a/src/functions/customer/controllers/product/update.spec.ts +++ b/src/functions/customer/controllers/product/update.spec.ts @@ -73,7 +73,6 @@ describe("CustomerProductControllerUpdate", () => { description: "hej med dig", }, context, - request, }); const res: HttpSuccessResponse = diff --git a/src/functions/customer/controllers/product/update.ts b/src/functions/customer/controllers/product/update.ts index a1426041..4bfed7ef 100644 --- a/src/functions/customer/controllers/product/update.ts +++ b/src/functions/customer/controllers/product/update.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { ScheduleProductZodSchema } from "~/functions/schedule/schedule.types"; -import { HttpRequest, InvocationContext } from "@azure/functions"; +import { InvocationContext } from "@azure/functions"; import { _ } from "~/library/handler"; import { GidFormat } from "~/library/zod"; import { CustomerProductUpdateOrchestration } from "../../orchestrations/product/update"; @@ -11,7 +11,6 @@ export type CustomerProductControllerUpdateRequest = { query: z.infer; body: z.infer; context: InvocationContext; - request: HttpRequest; }; const CustomerProductControllerUpdateQuerySchema = z.object({ @@ -33,12 +32,7 @@ export type CustomerProductControllerUpdateResponse = Awaited< >; export const CustomerProductControllerUpdate = _( - async ({ - query, - body, - request, - context, - }: CustomerProductControllerUpdateRequest) => { + async ({ query, body, context }: CustomerProductControllerUpdateRequest) => { const validateQuery = CustomerProductControllerUpdateQuerySchema.parse(query); const validateBody = CustomerProductControllerUpdateBodySchema.parse(body); @@ -49,6 +43,7 @@ export const CustomerProductControllerUpdate = _( ); await CustomerProductUpdateOrchestration(validateQuery, context); + return product; } ); diff --git a/src/functions/customer/orchestrations/customer/update.ts b/src/functions/customer/orchestrations/customer/update.ts index 0d95c164..6d10015d 100644 --- a/src/functions/customer/orchestrations/customer/update.ts +++ b/src/functions/customer/orchestrations/customer/update.ts @@ -34,7 +34,7 @@ const orchestrator: df.OrchestrationHandler = function* ( yield context.df.callActivity( updateArticleName, activityType({ - user, + customerId: user.customerId, }) ); diff --git a/src/functions/customer/orchestrations/customer/update/update-article.ts b/src/functions/customer/orchestrations/customer/update/update-article.ts index 6c61b4aa..ef056876 100644 --- a/src/functions/customer/orchestrations/customer/update/update-article.ts +++ b/src/functions/customer/orchestrations/customer/update/update-article.ts @@ -1,14 +1,16 @@ +import { CustomerServiceGet } from "~/functions/customer/services/customer/get"; import { CustomerLocationServiceList } from "~/functions/customer/services/location/list"; import { ScheduleModel } from "~/functions/schedule"; -import { User } from "~/functions/user"; import { shopifyRest } from "~/library/shopify/rest"; export const updateArticleName = "updateArticle"; export const updateArticle = async ({ - user, + customerId, }: { - user: User; + customerId: number; }): Promise => { + const user = await CustomerServiceGet({ customerId }); + const schedules = await ScheduleModel.find({ customerId: user.customerId, }); diff --git a/src/functions/customer/orchestrations/product/add.ts b/src/functions/customer/orchestrations/product/add.ts new file mode 100644 index 00000000..fd54d01c --- /dev/null +++ b/src/functions/customer/orchestrations/product/add.ts @@ -0,0 +1,57 @@ +import { InvocationContext } from "@azure/functions"; +import * as df from "durable-functions"; +import { OrchestrationContext } from "durable-functions"; +import { activityType } from "~/library/orchestration"; +import { + updateArticle, + updateArticleName, +} from "../customer/update/update-article"; +import { updatePrice, updatePriceName } from "./update/update-price"; +import { updateProduct, updateProductName } from "./update/update-product"; + +const orchestrator: df.OrchestrationHandler = function* ( + context: OrchestrationContext +) { + const input = context.df.getInput() as Input; + + const productUpdated: Awaited> = + yield context.df.callActivity( + updateProductName, + activityType(input) + ); + + const priceUpdated: Awaited> = + yield context.df.callActivity( + updatePriceName, + activityType(input) + ); + + const article: Awaited> = + yield context.df.callActivity( + updateArticleName, + activityType(input) + ); + + return { article, productUpdated, priceUpdated }; +}; + +df.app.orchestration("CustomerProductAddOrchestration", orchestrator); + +type Input = { productId: number; customerId: number }; + +export const CustomerProductAddOrchestration = async ( + input: Input, + context: InvocationContext +): Promise => { + const client = df.getClient(context); + const instanceId: string = await client.startNew( + "CustomerProductAddOrchestration", + { + input, + } + ); + + context.log(`Started orchestration with ID = '${instanceId}'.`); + + return instanceId; +}; diff --git a/src/functions/customer/orchestrations/product/destroy.ts b/src/functions/customer/orchestrations/product/destroy.ts new file mode 100644 index 00000000..fbea71ad --- /dev/null +++ b/src/functions/customer/orchestrations/product/destroy.ts @@ -0,0 +1,58 @@ +import { InvocationContext } from "@azure/functions"; +import * as df from "durable-functions"; +import { OrchestrationContext } from "durable-functions"; +import { ScheduleProduct } from "~/functions/schedule"; +import { activityType } from "~/library/orchestration"; +import { + destroyProductOption, + destroyProductOptionName, +} from "../product-options/destroy/destroy-option"; +import { destroyProduct, destroyProductName } from "./destroy/destroy-product"; + +df.app.activity(destroyProductName, { handler: destroyProduct }); + +const orchestrator: df.OrchestrationHandler = function* ( + context: OrchestrationContext +) { + const input = context.df.getInput() as Input; + + for (const productOption of input.product.options || []) { + yield context.df.callActivity( + destroyProductOptionName, + activityType({ + productOptionId: productOption.productId, + }) + ); + } + + const productDestroyed: Awaited> = + yield context.df.callActivity( + destroyProductName, + activityType(input.product) + ); + + return { productDestroyed }; +}; + +df.app.orchestration("CustomerProductDestroyOrchestration", orchestrator); + +type Input = { + product: ScheduleProduct; +}; + +export const CustomerProductDestroyOrchestration = async ( + input: Input, + context: InvocationContext +): Promise => { + const client = df.getClient(context); + const instanceId: string = await client.startNew( + "CustomerProductDestroyOrchestration", + { + input, + } + ); + + context.log(`Started orchestration with ID = '${instanceId}'.`); + + return instanceId; +}; diff --git a/src/functions/customer/orchestrations/product/destroy/destroy-product.spec.ts b/src/functions/customer/orchestrations/product/destroy/destroy-product.spec.ts new file mode 100644 index 00000000..a3f9822e --- /dev/null +++ b/src/functions/customer/orchestrations/product/destroy/destroy-product.spec.ts @@ -0,0 +1,48 @@ +import { shopifyAdmin } from "~/library/shopify"; +import { ProductOptionDestroyMutation } from "~/types/admin.generated"; +import { destroyProduct, PRODUCT_DESTROY } from "./destroy-product"; + +require("~/library/jest/mongoose/mongodb.jest"); + +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ + request: jest.fn(), + }), +})); + +const mockRequest = shopifyAdmin().request as jest.Mock; + +const productDelete: ProductOptionDestroyMutation = { + productDelete: { + deletedProductId: `gid://shopify/Product/123`, + }, +}; + +describe("CustomerProductDestroyOrchestration", () => { + beforeAll(async () => { + jest.clearAllMocks(); + }); + + it("destroyProduct", async () => { + mockRequest.mockResolvedValueOnce({ + data: { + productDelete: { + deletedProductId: { + id: "123", + }, + }, + }, + }); + + await destroyProduct({ + productId: 123, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_DESTROY, { + variables: { + productId: `gid://shopify/Product/123`, + }, + }); + }); +}); diff --git a/src/functions/customer/orchestrations/product/destroy/destroy-product.ts b/src/functions/customer/orchestrations/product/destroy/destroy-product.ts new file mode 100644 index 00000000..2a385a96 --- /dev/null +++ b/src/functions/customer/orchestrations/product/destroy/destroy-product.ts @@ -0,0 +1,20 @@ +import { shopifyAdmin } from "~/library/shopify"; + +export const destroyProductName = "destroyProductName"; +export const destroyProduct = async ({ productId }: { productId: number }) => { + const { data } = await shopifyAdmin().request(PRODUCT_DESTROY, { + variables: { + productId: `gid://shopify/Product/${productId}`, + }, + }); + + return data?.productDelete?.deletedProductId; +}; + +export const PRODUCT_DESTROY = `#graphql + mutation productDestroy($productId: ID!) { + productDelete(input: {id: $productId}) { + deletedProductId + } + } +` as const; diff --git a/src/functions/customer/orchestrations/product/update/update-product.spec.ts b/src/functions/customer/orchestrations/product/update/update-product.spec.ts index ebfc242d..3f8053af 100644 --- a/src/functions/customer/orchestrations/product/update/update-product.spec.ts +++ b/src/functions/customer/orchestrations/product/update/update-product.spec.ts @@ -202,6 +202,7 @@ describe("CustomerProductUpdateOrchestration", () => { id: mockProductUpdate.productUpdate?.product?.id, title: product.title, descriptionHtml: product.descriptionHtml, + handle: product.productHandle, metafields: [ { id: product?.hideFromProfileMetafieldId, diff --git a/src/functions/customer/services/product/destroy.spec.ts b/src/functions/customer/services/product/destroy.spec.ts index 4f0ba5b3..ce3923e2 100644 --- a/src/functions/customer/services/product/destroy.spec.ts +++ b/src/functions/customer/services/product/destroy.spec.ts @@ -1,19 +1,10 @@ import { TimeUnit } from "~/functions/schedule"; import { getProductObject } from "~/library/jest/helpers/product"; import { createScheduleWithProducts } from "~/library/jest/helpers/schedule"; -import { shopifyAdmin } from "~/library/shopify"; import { CustomerProductServiceDestroy } from "./destroy"; 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("CustomerProductServiceDestroy", () => { const customerId = 123; const name = "Test Schedule"; @@ -32,16 +23,6 @@ describe("CustomerProductServiceDestroy", () => { }); it("should remove an existing product from the schedule", async () => { - mockRequest.mockResolvedValueOnce({ - data: { - productDelete: { - deletedProductId: { - id: "123", - }, - }, - }, - }); - const newSchedule = await createScheduleWithProducts({ name, customerId, diff --git a/src/functions/customer/services/product/destroy.ts b/src/functions/customer/services/product/destroy.ts index 6f621687..740779d4 100644 --- a/src/functions/customer/services/product/destroy.ts +++ b/src/functions/customer/services/product/destroy.ts @@ -1,5 +1,4 @@ import { Schedule, ScheduleModel, ScheduleProduct } from "~/functions/schedule"; -import { shopifyAdmin } from "~/library/shopify"; export type CustomerProductServiceDestroyFilter = { customerId: Schedule["customerId"]; @@ -9,12 +8,6 @@ export type CustomerProductServiceDestroyFilter = { export const CustomerProductServiceDestroy = async ( filter: CustomerProductServiceDestroyFilter ) => { - await shopifyAdmin().request(PRODUCT_DESTROY, { - variables: { - productId: `gid://shopify/Product/${filter.productId}`, - }, - }); - return ScheduleModel.updateOne( { customerId: filter.customerId, @@ -26,13 +19,5 @@ export const CustomerProductServiceDestroy = async ( }, { $pull: { products: { productId: filter.productId } } }, { new: true } - ).lean(); + ); }; - -export const PRODUCT_DESTROY = `#graphql - mutation productDestroy($productId: ID!) { - productDelete(input: {id: $productId}) { - deletedProductId - } - } -` as const;