diff --git a/src/functions/customer-upload.orchestrators.ts b/src/functions/customer-upload.orchestrators.ts index 66e4c065..11e80ce3 100644 --- a/src/functions/customer-upload.orchestrators.ts +++ b/src/functions/customer-upload.orchestrators.ts @@ -19,6 +19,10 @@ import { updateArticle, updateArticleName, } from "./customer/orchestrations/customer/update/update-article"; +import { + updateUserMetaobject, + updateUserMetaobjectName, +} from "./customer/orchestrations/customer/update/update-user-metaobject"; df.app.activity("fileCreate", { handler: fileCreateHandler, @@ -68,7 +72,14 @@ df.app.orchestration("upload", function* (context: OrchestrationContext) { metaobjectId: response.id, }); - return yield context.df.callActivity( + yield context.df.callActivity( + updateUserMetaobjectName, + activityType({ + user, + }) + ); + + yield context.df.callActivity( updateArticleName, activityType({ user, diff --git a/src/functions/customer/controllers/product-options/add.ts b/src/functions/customer/controllers/product-options/add.ts index 580d4c6b..8f1386a6 100644 --- a/src/functions/customer/controllers/product-options/add.ts +++ b/src/functions/customer/controllers/product-options/add.ts @@ -1,12 +1,15 @@ import { z } from "zod"; +import { InvocationContext } from "@azure/functions"; import { _ } from "~/library/handler"; import { GidFormat } from "~/library/zod"; +import { CustomerProductOptionsAddOrchestration } from "../../orchestrations/product-options/add"; import { CustomerProductOptionsServiceAdd } from "../../services/product-options/add"; export type CustomerProductOptionsControllerAddRequest = { query: z.infer; body: z.infer; + context: InvocationContext; }; const CustomerProductOptionsControllerAddSchema = z.object({ @@ -20,15 +23,26 @@ const CustomerProductOptionsControllerAddBodySchema = z.object({ }); export const CustomerProductOptionsControllerAdd = _( - ({ query, body }: CustomerProductOptionsControllerAddRequest) => { + async ({ + query, + body, + context, + }: CustomerProductOptionsControllerAddRequest) => { const validateQuery = CustomerProductOptionsControllerAddSchema.parse(query); const validateBody = CustomerProductOptionsControllerAddBodySchema.parse(body); - return CustomerProductOptionsServiceAdd({ + const productOption = await CustomerProductOptionsServiceAdd({ ...validateQuery, ...validateBody, }); + + await CustomerProductOptionsAddOrchestration( + { productOptionId: productOption.productId, ...validateQuery }, + context + ); + + return context; } ); diff --git a/src/functions/customer/controllers/product-options/destroy.ts b/src/functions/customer/controllers/product-options/destroy.ts index c19ec72c..d61e8d61 100644 --- a/src/functions/customer/controllers/product-options/destroy.ts +++ b/src/functions/customer/controllers/product-options/destroy.ts @@ -1,11 +1,14 @@ import { z } from "zod"; +import { InvocationContext } from "@azure/functions"; import { _ } from "~/library/handler"; import { GidFormat } from "~/library/zod"; +import { CustomerProductOptionsDestroyOrchestration } from "../../orchestrations/product-options/destroy"; import { CustomerProductOptionsServiceDestroy } from "../../services/product-options/destroy"; export type CustomerProductOptionsControllerDestroyRequest = { query: z.infer; + context: InvocationContext; }; const CustomerProductOptionsControllerDestroySchema = z.object({ @@ -15,10 +18,18 @@ const CustomerProductOptionsControllerDestroySchema = z.object({ }); export const CustomerProductOptionsControllerDestroy = _( - ({ query }: CustomerProductOptionsControllerDestroyRequest) => { + async ({ + query, + context, + }: CustomerProductOptionsControllerDestroyRequest) => { const validateQuery = CustomerProductOptionsControllerDestroySchema.parse(query); + await CustomerProductOptionsDestroyOrchestration( + { productOptionId: validateQuery.optionProductId }, + context + ); + return CustomerProductOptionsServiceDestroy(validateQuery); } ); diff --git a/src/functions/customer/controllers/product/add.spec.ts b/src/functions/customer/controllers/product/add.spec.ts index bdb7e55a..cb995bbf 100644 --- a/src/functions/customer/controllers/product/add.spec.ts +++ b/src/functions/customer/controllers/product/add.spec.ts @@ -29,13 +29,13 @@ jest.mock("../../orchestrations/product/update", () => ({ }), })); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerProductControllerAdd", () => { let context: InvocationContext; diff --git a/src/functions/customer/controllers/product/destroy.spec.ts b/src/functions/customer/controllers/product/destroy.spec.ts index 6d4420dd..66bc60bd 100644 --- a/src/functions/customer/controllers/product/destroy.spec.ts +++ b/src/functions/customer/controllers/product/destroy.spec.ts @@ -18,13 +18,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerProductControllerDestroy", () => { let context: InvocationContext; diff --git a/src/functions/customer/controllers/schedule/create.spec.ts b/src/functions/customer/controllers/schedule/create.spec.ts index 5441a072..9c14d766 100644 --- a/src/functions/customer/controllers/schedule/create.spec.ts +++ b/src/functions/customer/controllers/schedule/create.spec.ts @@ -15,13 +15,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleControllerCreate", () => { let context: InvocationContext; diff --git a/src/functions/customer/controllers/schedule/destroy.spec.ts b/src/functions/customer/controllers/schedule/destroy.spec.ts index 2dffaf41..b1705206 100644 --- a/src/functions/customer/controllers/schedule/destroy.spec.ts +++ b/src/functions/customer/controllers/schedule/destroy.spec.ts @@ -16,13 +16,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleControllerDestroy", () => { let context: InvocationContext; diff --git a/src/functions/customer/controllers/schedule/update.spec.ts b/src/functions/customer/controllers/schedule/update.spec.ts index b19182b8..e3c4af46 100644 --- a/src/functions/customer/controllers/schedule/update.spec.ts +++ b/src/functions/customer/controllers/schedule/update.spec.ts @@ -18,13 +18,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleControllerUpdate", () => { let context: InvocationContext; diff --git a/src/functions/customer/controllers/slot/update.spec.ts b/src/functions/customer/controllers/slot/update.spec.ts index 23bf5b4f..a5eb2b3e 100644 --- a/src/functions/customer/controllers/slot/update.spec.ts +++ b/src/functions/customer/controllers/slot/update.spec.ts @@ -19,13 +19,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleSlotControllerUpdate", () => { let context: InvocationContext; diff --git a/src/functions/customer/controllers/upload/file-create.ts b/src/functions/customer/controllers/upload/file-create.ts index ec33c9c8..e192897f 100644 --- a/src/functions/customer/controllers/upload/file-create.ts +++ b/src/functions/customer/controllers/upload/file-create.ts @@ -2,7 +2,7 @@ import { shopifyAdmin } from "~/library/shopify"; import { FileInputProps, getFilenameFromUrl } from "./types"; export async function fileCreateHandler(input: FileInputProps) { - const { data } = await shopifyAdmin.request(FILE_CREATE, { + const { data } = await shopifyAdmin().request(FILE_CREATE, { variables: { files: { alt: getFilenameFromUrl(input.resourceUrl), diff --git a/src/functions/customer/controllers/upload/file-get.ts b/src/functions/customer/controllers/upload/file-get.ts index 3e5bc9b1..ecd9e296 100644 --- a/src/functions/customer/controllers/upload/file-get.ts +++ b/src/functions/customer/controllers/upload/file-get.ts @@ -1,7 +1,7 @@ import { shopifyAdmin } from "~/library/shopify"; export async function fileGetHandler(metaobjectId: string) { - const { data } = await shopifyAdmin.request(FILE_GET, { + const { data } = await shopifyAdmin().request(FILE_GET, { variables: { id: metaobjectId, }, diff --git a/src/functions/customer/controllers/upload/resource-url.ts b/src/functions/customer/controllers/upload/resource-url.ts index 2fab2d57..3d0e2c1a 100644 --- a/src/functions/customer/controllers/upload/resource-url.ts +++ b/src/functions/customer/controllers/upload/resource-url.ts @@ -21,7 +21,7 @@ export const CustomerUploadControllerResourceURL = _( async ({ query }: CustomerUploadControllerResourceURLRequest) => { const validateData = CustomerUploadControllerResourceURLQuerySchema.parse(query); - const { data } = await shopifyAdmin.request(UPLOAD_CREATE, { + const { data } = await shopifyAdmin().request(UPLOAD_CREATE, { variables: { input: [ { diff --git a/src/functions/customer/orchestrations/customer/create.ts b/src/functions/customer/orchestrations/customer/create.ts index b5157799..a16d95dd 100644 --- a/src/functions/customer/orchestrations/customer/create.ts +++ b/src/functions/customer/orchestrations/customer/create.ts @@ -1,28 +1,32 @@ import { InvocationContext } from "@azure/functions"; +import { addSeconds } from "date-fns"; import * as df from "durable-functions"; import { OrchestrationContext } from "durable-functions"; import { User } from "~/functions/user"; import { activityType } from "~/library/orchestration"; -import { createArticle } from "./create/create-article"; -import { createCollection } from "./create/create-collection"; -import { createUserMetaobject } from "./create/create-user-metaobject"; -import { publishCollection } from "./create/publish-collection"; -import { updateUser } from "./create/update-user"; +import { createArticle, createArticleName } from "./create/create-article"; +import { + createCollection, + createCollectionName, +} from "./create/create-collection"; +import { + createUserMetaobject, + createUserMetaobjectName, +} from "./create/create-user-metaobject"; +import { + publishCollection, + publishCollectionName, +} from "./create/publish-collection"; +import { + updateUserMetafields, + updateUserMetafieldsName, +} from "./create/update-user"; -const createCollectionName = "createCollection"; df.app.activity(createCollectionName, { handler: createCollection }); - -const createUserMetaobjectName = "createUserMetaobject"; df.app.activity(createUserMetaobjectName, { handler: createUserMetaobject }); - -const createArticleName = "createArticle"; df.app.activity(createArticleName, { handler: createArticle }); - -const publishCollectionName = "publishCollection"; df.app.activity(publishCollectionName, { handler: publishCollection }); - -const updateUserName = "updateUser"; -df.app.activity(updateUserName, { handler: updateUser }); +df.app.activity(updateUserMetafieldsName, { handler: updateUserMetafields }); const orchestrator: df.OrchestrationHandler = function* ( context: OrchestrationContext @@ -58,18 +62,10 @@ const orchestrator: df.OrchestrationHandler = function* ( }) ); - const publish: Awaited> = + const userUpdated: Awaited> = yield context.df.callActivity( - publishCollectionName, - activityType({ - collectionId: collectionMetaobject.id, - }) - ); - - const userUpdated: Awaited> = - yield context.df.callActivity( - updateUserName, - activityType({ + updateUserMetafieldsName, + activityType({ collectionMetaobjectId: collectionMetaobject.id, userId: user._id, userMetaobjectId: userMetaobject.id, @@ -77,6 +73,17 @@ const orchestrator: df.OrchestrationHandler = function* ( }) ); + const nextExecution = addSeconds(context.df.currentUtcDateTime, 5); + yield context.df.createTimer(nextExecution); + + const publish: Awaited> = + yield context.df.callActivity( + publishCollectionName, + activityType({ + collectionId: collectionMetaobject.id, + }) + ); + return { collectionMetaobject, userMetaobject, diff --git a/src/functions/customer/orchestrations/customer/create/create-article.ts b/src/functions/customer/orchestrations/customer/create/create-article.ts index d71ad5a5..4dbff01e 100644 --- a/src/functions/customer/orchestrations/customer/create/create-article.ts +++ b/src/functions/customer/orchestrations/customer/create/create-article.ts @@ -1,34 +1,7 @@ import { User } from "~/functions/user"; import { shopifyRest } from "~/library/shopify/rest"; -interface RootObject { - article: Article; -} -interface Article { - id: number; - title: string; - created_at: string; - body_html: string; - blog_id: number; - author: string; - user_id?: any; - published_at: string; - updated_at: string; - summary_html: string; - template_suffix?: any; - handle: string; - tags: string; - admin_graphql_api_id: string; - image: Image; -} -interface Image { - created_at: string; - alt: string; - width: number; - height: number; - src: string; -} - +export const createArticleName = "createArticle"; export const createArticle = async ({ user, }: { @@ -84,7 +57,7 @@ export const createArticle = async ({ ) ); - const response = await shopifyRest.post("blogs/105364226375/articles", { + const response = await shopifyRest().post("blogs/105364226375/articles", { data: { article: { blog_id: 105364226375, @@ -116,3 +89,31 @@ export const createArticle = async ({ return await response.json(); }; + +interface RootObject { + article: Article; +} +interface Article { + id: number; + title: string; + created_at: string; + body_html: string; + blog_id: number; + author: string; + user_id?: any; + published_at: string; + updated_at: string; + summary_html: string; + template_suffix?: any; + handle: string; + tags: string; + admin_graphql_api_id: string; + image: Image; +} +interface Image { + created_at: string; + alt: string; + width: number; + height: number; + src: string; +} diff --git a/src/functions/customer/orchestrations/customer/create/create-collection.spec.ts b/src/functions/customer/orchestrations/customer/create/create-collection.spec.ts index f7b37bfc..29edb130 100644 --- a/src/functions/customer/orchestrations/customer/create/create-collection.spec.ts +++ b/src/functions/customer/orchestrations/customer/create/create-collection.spec.ts @@ -6,13 +6,13 @@ import { COLLECTION_CREATE, createCollection } from "./create-collection"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +let mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerCreateOrchestration", () => { beforeAll(async () => { @@ -39,9 +39,9 @@ describe("CustomerCreateOrchestration", () => { await createCollection({ user }); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith(1, COLLECTION_CREATE, { + expect(mockRequest).toHaveBeenNthCalledWith(1, COLLECTION_CREATE, { variables: { input: { handle: user.username, diff --git a/src/functions/customer/orchestrations/customer/create/create-collection.ts b/src/functions/customer/orchestrations/customer/create/create-collection.ts index 363eab90..3f180c5c 100644 --- a/src/functions/customer/orchestrations/customer/create/create-collection.ts +++ b/src/functions/customer/orchestrations/customer/create/create-collection.ts @@ -1,12 +1,13 @@ import { User } from "~/functions/user"; import { shopifyAdmin } from "~/library/shopify"; +export const createCollectionName = "createCollection"; export const createCollection = async ({ user, }: { user: Pick; }) => { - const { data } = await shopifyAdmin.request(COLLECTION_CREATE, { + const { data } = await shopifyAdmin().request(COLLECTION_CREATE, { variables: { input: { handle: user.username, diff --git a/src/functions/customer/orchestrations/customer/create/create-user-metaobject.spec.ts b/src/functions/customer/orchestrations/customer/create/create-user-metaobject.spec.ts index d6ef1b9a..245e80e0 100644 --- a/src/functions/customer/orchestrations/customer/create/create-user-metaobject.spec.ts +++ b/src/functions/customer/orchestrations/customer/create/create-user-metaobject.spec.ts @@ -9,13 +9,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerCreateOrchestration", () => { beforeAll(async () => { @@ -76,46 +76,46 @@ describe("CustomerCreateOrchestration", () => { collectionId: "gid://shopify/Collection/625094558023", }); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - CREATE_USER_METAOBJECT, - { - variables: { - handle: userData.username, - fields: [ - { - key: "username", - value: userData.username, - }, - { - key: "fullname", - value: userData.fullname, - }, - { - key: "short_description", - value: userData.shortDescription || "", - }, - { - key: "about_me", - value: userData.aboutMeHtml || "", - }, - { - key: "professions", - value: JSON.stringify(userData.professions || []), - }, - { - key: "collection", - value: collectionMetaobjectId, - }, - { - key: "theme", - value: "pink", - }, - ], - }, - } - ); + expect(mockRequest).toHaveBeenNthCalledWith(1, CREATE_USER_METAOBJECT, { + variables: { + handle: userData.username, + fields: [ + { + key: "username", + value: userData.username, + }, + { + key: "fullname", + value: userData.fullname, + }, + { + key: "short_description", + value: userData.shortDescription || "", + }, + { + key: "about_me", + value: userData.aboutMeHtml || "", + }, + { + key: "professions", + value: JSON.stringify(userData.professions || []), + }, + { + key: "collection", + value: collectionMetaobjectId, + }, + { + key: "theme", + value: "pink", + }, + { + key: "active", + value: "False", + }, + ], + }, + }); }); }); diff --git a/src/functions/customer/orchestrations/customer/create/create-user-metaobject.ts b/src/functions/customer/orchestrations/customer/create/create-user-metaobject.ts index ce0d4fa6..deaad279 100644 --- a/src/functions/customer/orchestrations/customer/create/create-user-metaobject.ts +++ b/src/functions/customer/orchestrations/customer/create/create-user-metaobject.ts @@ -1,6 +1,7 @@ import { User } from "~/functions/user"; import { shopifyAdmin } from "~/library/shopify"; +export const createUserMetaobjectName = "createUserMetaobject"; export const createUserMetaobject = async ({ user, collectionId, @@ -8,7 +9,7 @@ export const createUserMetaobject = async ({ user: Omit; collectionId: string; }) => { - const { data } = await shopifyAdmin.request(CREATE_USER_METAOBJECT, { + const { data } = await shopifyAdmin().request(CREATE_USER_METAOBJECT, { variables: { handle: user.username, fields: [ @@ -40,6 +41,10 @@ export const createUserMetaobject = async ({ key: "theme", value: "pink", }, + { + key: "active", + value: "False", + }, ], }, }); diff --git a/src/functions/customer/orchestrations/customer/create/public-collection.spec.ts b/src/functions/customer/orchestrations/customer/create/public-collection.spec.ts index b8bf9aa3..67386450 100644 --- a/src/functions/customer/orchestrations/customer/create/public-collection.spec.ts +++ b/src/functions/customer/orchestrations/customer/create/public-collection.spec.ts @@ -9,13 +9,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerCreateOrchestration", () => { beforeAll(async () => { @@ -59,12 +59,12 @@ describe("CustomerCreateOrchestration", () => { collectionId, }); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(8); + expect(mockRequest).toHaveBeenCalledTimes(8); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith(1, PUBLICATIONS); + expect(mockRequest).toHaveBeenNthCalledWith(1, PUBLICATIONS); mockPublications.map((p, index) => { - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( + expect(mockRequest).toHaveBeenNthCalledWith( 2 + index, PUBLISH_COLLECTION, { diff --git a/src/functions/customer/orchestrations/customer/create/publish-collection.ts b/src/functions/customer/orchestrations/customer/create/publish-collection.ts index cce1583a..6ee340f7 100644 --- a/src/functions/customer/orchestrations/customer/create/publish-collection.ts +++ b/src/functions/customer/orchestrations/customer/create/publish-collection.ts @@ -1,11 +1,12 @@ import { shopifyAdmin } from "~/library/shopify"; +export const publishCollectionName = "publishCollection"; export const publishCollection = async ({ collectionId, }: { collectionId: string; }) => { - const { data } = await shopifyAdmin.request(PUBLICATIONS); + const { data } = await shopifyAdmin().request(PUBLICATIONS); if (!data?.publications.nodes) { throw new Error(`Failed to find any publichations for ${collectionId}`); @@ -13,7 +14,7 @@ export const publishCollection = async ({ await Promise.all( data.publications.nodes.map(async (pub) => { - return shopifyAdmin.request(PUBLISH_COLLECTION, { + return shopifyAdmin().request(PUBLISH_COLLECTION, { variables: { collectionId: collectionId, publicationId: pub.id, diff --git a/src/functions/customer/orchestrations/customer/create/update-user.ts b/src/functions/customer/orchestrations/customer/create/update-user.ts index 1bd87cbd..a46fda1d 100644 --- a/src/functions/customer/orchestrations/customer/create/update-user.ts +++ b/src/functions/customer/orchestrations/customer/create/update-user.ts @@ -1,6 +1,7 @@ import { UserModel } from "~/functions/user"; -export const updateUser = async ({ +export const updateUserMetafieldsName = "updateUserMetafields"; +export const updateUserMetafields = async ({ userId, collectionMetaobjectId, userMetaobjectId, diff --git a/src/functions/customer/orchestrations/customer/update.ts b/src/functions/customer/orchestrations/customer/update.ts index 2af17a13..0d95c164 100644 --- a/src/functions/customer/orchestrations/customer/update.ts +++ b/src/functions/customer/orchestrations/customer/update.ts @@ -4,9 +4,11 @@ import { OrchestrationContext } from "durable-functions"; import { User } from "~/functions/user"; import { activityType } from "~/library/orchestration"; import { updateArticle, updateArticleName } from "./update/update-article"; -import { updateUserMetaobject } from "./update/update-user-metaobject"; +import { + updateUserMetaobject, + updateUserMetaobjectName, +} from "./update/update-user-metaobject"; -const updateUserMetaobjectName = "updateUserMetaobject"; df.app.activity(updateUserMetaobjectName, { handler: updateUserMetaobject, }); diff --git a/src/functions/customer/orchestrations/customer/update/update-article.ts b/src/functions/customer/orchestrations/customer/update/update-article.ts index 1633ad26..6c61b4aa 100644 --- a/src/functions/customer/orchestrations/customer/update/update-article.ts +++ b/src/functions/customer/orchestrations/customer/update/update-article.ts @@ -4,7 +4,6 @@ import { User } from "~/functions/user"; import { shopifyRest } from "~/library/shopify/rest"; export const updateArticleName = "updateArticle"; - export const updateArticle = async ({ user, }: { @@ -52,7 +51,7 @@ export const updateArticle = async ({ tags.push(`gender-${user.gender}`); - const response = await shopifyRest.put( + const response = await shopifyRest().put( `blogs/105364226375/articles/${user.articleId}`, { data: { 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 9850aa1d..fb7a58aa 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 @@ -9,13 +9,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerUpdateOrchestration", () => { beforeAll(async () => { @@ -42,46 +42,42 @@ describe("CustomerUpdateOrchestration", () => { await updateUserMetaobject({ user }); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - UPDATE_USER_METAOBJECT, - { - variables: { - id: user.userMetaobjectId || "", - fields: [ - { - key: "fullname", - value: user.fullname, - }, - { - key: "short_description", - value: user.shortDescription || "", - }, - { - key: "about_me", - value: user.aboutMeHtml || "", - }, - { - key: "professions", - value: JSON.stringify(user.professions || []), - }, - { - key: "social", - value: JSON.stringify(user.social), - }, - { - key: "active", - value: String(user.active), - }, - { - key: "theme", - value: user.theme?.color || "pink", - }, - ], - }, - } - ); + expect(mockRequest).toHaveBeenNthCalledWith(1, UPDATE_USER_METAOBJECT, { + variables: { + id: user.userMetaobjectId || "", + fields: [ + { + key: "fullname", + value: user.fullname, + }, + { + key: "short_description", + value: user.shortDescription || "", + }, + { + key: "about_me", + value: user.aboutMeHtml || "", + }, + { + key: "professions", + value: JSON.stringify(user.professions || []), + }, + { + key: "social", + value: JSON.stringify(user.social), + }, + { + key: "active", + value: String(user.active), + }, + { + key: "theme", + value: user.theme?.color || "pink", + }, + ], + }, + }); }); }); 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 99b1747a..30eb7af3 100644 --- a/src/functions/customer/orchestrations/customer/update/update-user-metaobject.ts +++ b/src/functions/customer/orchestrations/customer/update/update-user-metaobject.ts @@ -1,6 +1,7 @@ import { User } from "~/functions/user"; import { shopifyAdmin } from "~/library/shopify"; +export const updateUserMetaobjectName = "updateUserMetaobject"; export const updateUserMetaobject = async ({ user, }: { @@ -48,7 +49,7 @@ export const updateUserMetaobject = async ({ ], }; - const { data } = await shopifyAdmin.request(UPDATE_USER_METAOBJECT, { + const { data } = await shopifyAdmin().request(UPDATE_USER_METAOBJECT, { variables, }); diff --git a/src/functions/customer/orchestrations/product-options/add.ts b/src/functions/customer/orchestrations/product-options/add.ts new file mode 100644 index 00000000..b828c98b --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/add.ts @@ -0,0 +1,60 @@ +import { InvocationContext } from "@azure/functions"; +import * as df from "durable-functions"; +import { OrchestrationContext } from "durable-functions"; +import { activityType } from "~/library/orchestration"; +import { + updateParentProduct, + updateParentProductName, +} from "./add/update-parent-product"; +import { + updateProductOption, + updateProductOptionName, +} from "./add/update-product-option"; + +df.app.activity(updateParentProductName, { handler: updateParentProduct }); +df.app.activity(updateProductOptionName, { handler: updateProductOption }); + +const orchestrator: df.OrchestrationHandler = function* ( + context: OrchestrationContext +) { + const input = context.df.getInput() as Input; + + const parentProduct: Awaited> = + yield context.df.callActivity( + updateParentProductName, + activityType(input) + ); + + const productOption: Awaited> = + yield context.df.callActivity( + updateProductOptionName, + activityType(input) + ); + + return { parentProduct, productOption }; +}; + +df.app.orchestration("CustomerProductOptionsAddOrchestration", orchestrator); + +type Input = { + productOptionId: number; + customerId: number; + productId: number; +}; + +export const CustomerProductOptionsAddOrchestration = async ( + input: Input, + context: InvocationContext +): Promise => { + const client = df.getClient(context); + const instanceId: string = await client.startNew( + "CustomerProductOptionsAddOrchestration", + { + input, + } + ); + + context.log(`Started orchestration with ID = '${instanceId}'.`); + + return instanceId; +}; diff --git a/src/functions/customer/orchestrations/product-options/add/update-parent-product.spec.ts b/src/functions/customer/orchestrations/product-options/add/update-parent-product.spec.ts new file mode 100644 index 00000000..28eca338 --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/add/update-parent-product.spec.ts @@ -0,0 +1,72 @@ +import { createUser } from "~/library/jest/helpers"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { createScheduleWithProducts } from "~/library/jest/helpers/schedule"; +import { shopifyAdmin } from "~/library/shopify"; +import { ProductParentUpdateMutation } from "~/types/admin.generated"; +import { + PRODUCT_PARENT_UPDATE, + updateParentProduct, +} from "./update-parent-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 mockProductParentUpdate: ProductParentUpdateMutation = { + productUpdate: { + product: { + options: { + id: "gid://shopify/Metafield/44505109102919", + }, + }, + }, +}; + +describe("CustomerProductOptionsAddOrchestration", () => { + beforeAll(async () => { + jest.clearAllMocks(); + }); + + it("updateParentProduct", async () => { + const customerId = 123; + + const user = await createUser({ customerId }); + + const product = { ...getProductObject({}) }; + + const newSchedule = await createScheduleWithProducts({ + name: "Test Schedule", + customerId, + products: [product], + }); + + mockRequest.mockResolvedValueOnce({ + data: mockProductParentUpdate, + }); + + await updateParentProduct({ + customerId, + productId: product.productId, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_PARENT_UPDATE, { + variables: { + id: `gid://shopify/Product/${product.productId}`, + metafields: [ + { + key: "options", + namespace: "booking", + value: JSON.stringify([]), + }, + ], + }, + }); + }); +}); diff --git a/src/functions/customer/orchestrations/product-options/add/update-parent-product.ts b/src/functions/customer/orchestrations/product-options/add/update-parent-product.ts new file mode 100644 index 00000000..ba554fc8 --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/add/update-parent-product.ts @@ -0,0 +1,77 @@ +import { CustomerProductServiceGet } from "~/functions/customer/services/product/get"; +import { ScheduleModel, ScheduleProduct } from "~/functions/schedule"; +import { shopifyAdmin } from "~/library/shopify"; + +export const updateParentProductName = "updateParentProduct"; +export const updateParentProduct = async ({ + customerId, + productId, +}: { + customerId: number; + productId: number; +}) => { + const rootProduct = await CustomerProductServiceGet({ + customerId, + productId, + }); + + const optionMetafield = rootProduct.optionsMetafieldId + ? { + id: rootProduct.optionsMetafieldId, + } + : { + key: "options", + namespace: "booking", + }; + + const { data } = await shopifyAdmin().request(PRODUCT_PARENT_UPDATE, { + variables: { + id: `gid://shopify/Product/${productId}`, + metafields: [ + { + ...optionMetafield, + value: JSON.stringify( + rootProduct?.options?.map( + (o) => `gid://shopify/Product/${o.productId}` + ) + ), + }, + ], + }, + }); + + if (!data?.productUpdate?.product) { + throw new Error(`Failed to update parent product options${productId}`); + } + + const newProduct: ScheduleProduct = { + ...rootProduct, + optionsMetafieldId: data?.productUpdate?.product?.options?.id, + }; + + await ScheduleModel.updateOne( + { + customerId, + "products.productId": productId, + }, + { + $set: { + "products.$": newProduct, + }, + } + ); + + return data.productUpdate.product; +}; + +export const PRODUCT_PARENT_UPDATE = `#graphql + mutation ProductParentUpdate($id: ID, $metafields: [MetafieldInput!]) { + productUpdate(input: {id: $id, metafields: $metafields}) { + product { + options: metafield(key: "options", namespace: "booking") { + id + } + } + } + } +` as const; diff --git a/src/functions/customer/orchestrations/product-options/add/update-product-option.spec.ts b/src/functions/customer/orchestrations/product-options/add/update-product-option.spec.ts new file mode 100644 index 00000000..a5ee3b34 --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/add/update-product-option.spec.ts @@ -0,0 +1,161 @@ +import { createUser } from "~/library/jest/helpers"; +import { getProductObject } from "~/library/jest/helpers/product"; +import { createScheduleWithProducts } from "~/library/jest/helpers/schedule"; +import { shopifyAdmin } from "~/library/shopify"; +import { ProductOptionAddMutation } from "~/types/admin.generated"; +import { + PRODUCT_OPTION_ADD, + updateProductOption, +} from "./update-product-option"; + +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("CustomerProductOptionsAddOrchestration", () => { + beforeAll(async () => { + jest.clearAllMocks(); + }); + + it("updateProductOption", async () => { + const customerId = 123; + + const user = await createUser({ customerId }); + + const option = { + parentIdMetafieldId: "gid://shopify/Product/123123312", + productId: 123, + title: "new", + required: true, + variants: [ + { + variantId: 1, + title: "a", + price: 1, + duration: { + metafieldId: 11, + value: 60, + }, + }, + { + variantId: 2, + title: "b", + price: 2, + duration: { + metafieldId: 22, + value: 30, + }, + }, + { + variantId: 3, + title: "c", + price: 2, + duration: { + metafieldId: 33, + value: 15, + }, + }, + ], + }; + + const product = getProductObject({ + productId: 321, + options: [option], + }); + + const newSchedule = await createScheduleWithProducts({ + name: "Test Schedule", + customerId, + products: [product], + }); + + const tags = [ + `user`, + `user-${user.username}`, + `userid-${customerId}`, + `options`, + `parentid-${product.productId}`, + `parent-${product.productHandle}`, + ]; + + const mockProductOptionUpdate: ProductOptionAddMutation = { + productUpdate: { + product: { + id: "gid://shopify/Product/9186772386119", + title: "New Product Title", + handle: "ikadsk", + tags, + required: { + id: "gid://shopify/Metafield/12", + value: "true", + }, + parentId: { + id: "gid://shopify/Metafield/44499605258567", + value: `gid://shopify/Product/${product.productId}`, + }, + variants: { + nodes: [ + { + id: "gid://shopify/ProductVariant/49475617128775", + title: "Tyk", + price: "12.00", + duration: { + id: "gid://shopify/Metafield/3", + value: "1", + }, + }, + { + id: "gid://shopify/ProductVariant/49475617259847", + title: "Normal", + price: "12.00", + duration: { + id: "gid://shopify/Metafield/2", + value: "2", + }, + }, + { + id: "gid://shopify/ProductVariant/49475617358151", + title: "Meget tyk", + price: "12.00", + duration: { + id: "gid://shopify/Metafield/1", + value: "3", + }, + }, + ], + }, + }, + }, + }; + + mockRequest.mockResolvedValueOnce({ + data: mockProductOptionUpdate, + }); + + await updateProductOption({ + customerId, + productId: product.productId, + productOptionId: option.productId, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_OPTION_ADD, { + variables: { + id: `gid://shopify/Product/${option.productId}`, + metafields: [ + { + id: option.parentIdMetafieldId, + value: `gid://shopify/Product/${product.productId}`, + }, + ], + tags: tags.join(", "), + }, + }); + }); +}); diff --git a/src/functions/customer/orchestrations/product-options/add/update-product-option.ts b/src/functions/customer/orchestrations/product-options/add/update-product-option.ts new file mode 100644 index 00000000..a9167188 --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/add/update-product-option.ts @@ -0,0 +1,71 @@ +import { CustomerServiceGet } from "~/functions/customer/services/customer/get"; +import { PRODUCT_OPTION_FRAGMENT } from "~/functions/customer/services/product-options/add"; +import { CustomerProductServiceGet } from "~/functions/customer/services/product/get"; +import { shopifyAdmin } from "~/library/shopify"; + +export const updateProductOptionName = "updateProductOption"; +export const updateProductOption = async ({ + productOptionId, + customerId, + productId, +}: { + productOptionId: number; + customerId: number; + productId: number; +}) => { + const rootProduct = await CustomerProductServiceGet({ + customerId, + productId, + }); + + const productOption = rootProduct.options?.find( + (p) => p.productId === productOptionId + ); + + if (!productOption) { + throw new Error(`Failed to find product option ${productOptionId}`); + } + + const user = await CustomerServiceGet({ + customerId, + }); + + const { data } = await shopifyAdmin().request(PRODUCT_OPTION_ADD, { + variables: { + id: `gid://shopify/Product/${productOption.productId}`, + metafields: [ + { + id: productOption.parentIdMetafieldId, + value: `gid://shopify/Product/${productId}`, + }, + ], + tags: [ + `user`, + `user-${user.username}`, + `userid-${customerId}`, + `options`, + `parentid-${rootProduct.productId}`, + `parent-${rootProduct.productHandle}`, + ].join(", "), + }, + }); + + if (!data?.productUpdate?.product) { + throw new Error( + `Failed to update product option ${productOption.productId}` + ); + } + + return data.productUpdate.product; +}; + +export const PRODUCT_OPTION_ADD = `#graphql + ${PRODUCT_OPTION_FRAGMENT} + mutation ProductOptionAdd($id: ID!, $metafields: [MetafieldInput!]!, $tags: [String!]!) { + productUpdate(input: {id: $id, metafields: $metafields, tags: $tags}) { + product { + ...ProductOptionFragment + } + } + } +` as const; diff --git a/src/functions/customer/orchestrations/product-options/destroy.ts b/src/functions/customer/orchestrations/product-options/destroy.ts new file mode 100644 index 00000000..24f1ee04 --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/destroy.ts @@ -0,0 +1,48 @@ +import { InvocationContext } from "@azure/functions"; +import * as df from "durable-functions"; +import { OrchestrationContext } from "durable-functions"; +import { activityType } from "~/library/orchestration"; +import { + destroyProductOption, + destroyProductOptionName, +} from "./destroy/destroy-option"; + +const orchestrator: df.OrchestrationHandler = function* ( + context: OrchestrationContext +) { + const input = context.df.getInput() as Input; + + const destroyOption: Awaited> = + yield context.df.callActivity( + destroyProductOptionName, + activityType(input) + ); + + return { destroyOption }; +}; + +df.app.orchestration( + "CustomerProductOptionsDestroyOrchestration", + orchestrator +); + +type Input = { + productOptionId: number; +}; + +export const CustomerProductOptionsDestroyOrchestration = async ( + input: Input, + context: InvocationContext +): Promise => { + const client = df.getClient(context); + const instanceId: string = await client.startNew( + "CustomerProductOptionsDestroyOrchestration", + { + input, + } + ); + + context.log(`Started orchestration with ID = '${instanceId}'.`); + + return instanceId; +}; diff --git a/src/functions/customer/orchestrations/product-options/destroy/destroy-option.spec.ts b/src/functions/customer/orchestrations/product-options/destroy/destroy-option.spec.ts new file mode 100644 index 00000000..7f068528 --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/destroy/destroy-option.spec.ts @@ -0,0 +1,42 @@ +import { shopifyAdmin } from "~/library/shopify"; +import { ProductOptionDestroyMutation } from "~/types/admin.generated"; +import { destroyProductOption, PRODUCT_OPTION_DESTROY } from "./destroy-option"; + +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("CustomerProductOptionsDestroyOrchestration", () => { + beforeAll(async () => { + jest.clearAllMocks(); + }); + + it("destroyProductOption", async () => { + mockRequest.mockResolvedValueOnce({ + data: productDelete, + }); + + await destroyProductOption({ + productOptionId: 123, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_OPTION_DESTROY, { + variables: { + productId: `gid://shopify/Product/123`, + }, + }); + }); +}); diff --git a/src/functions/customer/orchestrations/product-options/destroy/destroy-option.ts b/src/functions/customer/orchestrations/product-options/destroy/destroy-option.ts new file mode 100644 index 00000000..6d42f1fe --- /dev/null +++ b/src/functions/customer/orchestrations/product-options/destroy/destroy-option.ts @@ -0,0 +1,24 @@ +import { shopifyAdmin } from "~/library/shopify"; + +export const destroyProductOptionName = "destroyProductOptionName"; +export const destroyProductOption = async ({ + productOptionId, +}: { + productOptionId: number; +}) => { + const { data } = await shopifyAdmin().request(PRODUCT_OPTION_DESTROY, { + variables: { + productId: `gid://shopify/Product/${productOptionId}`, + }, + }); + + return data?.productDelete?.deletedProductId; +}; + +export const PRODUCT_OPTION_DESTROY = `#graphql + mutation productOptionDestroy($productId: ID!) { + productDelete(input: {id: $productId}) { + deletedProductId + } + } +` as const; diff --git a/src/functions/customer/orchestrations/product/update.ts b/src/functions/customer/orchestrations/product/update.ts index d8689aa2..88c12f57 100644 --- a/src/functions/customer/orchestrations/product/update.ts +++ b/src/functions/customer/orchestrations/product/update.ts @@ -2,13 +2,10 @@ import { InvocationContext } from "@azure/functions"; import * as df from "durable-functions"; import { OrchestrationContext } from "durable-functions"; import { activityType } from "~/library/orchestration"; -import { updatePrice } from "./update/update-price"; -import { updateProduct } from "./update/update-product"; +import { updatePrice, updatePriceName } from "./update/update-price"; +import { updateProduct, updateProductName } from "./update/update-product"; -const updateProductName = "updateProduct"; df.app.activity(updateProductName, { handler: updateProduct }); - -const updatePriceName = "updatePrice"; df.app.activity(updatePriceName, { handler: updatePrice }); const orchestrator: df.OrchestrationHandler = function* ( diff --git a/src/functions/customer/orchestrations/product/update/update-price.spec.ts b/src/functions/customer/orchestrations/product/update/update-price.spec.ts index fc9d5fa1..b59f90de 100644 --- a/src/functions/customer/orchestrations/product/update/update-price.spec.ts +++ b/src/functions/customer/orchestrations/product/update/update-price.spec.ts @@ -12,13 +12,13 @@ import { PRODUCT_PRICE_UPDATE, updatePrice } from "./update-price"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; const mockProductPriceUpdate: ProductPricepdateMutation = { productVariantsBulkUpdate: { @@ -91,23 +91,19 @@ describe("CustomerProductUpdateOrchestration", () => { productId: product.productId, }); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - PRODUCT_PRICE_UPDATE, - { - variables: { - id: mockProductPriceUpdate.productVariantsBulkUpdate?.product?.id, - variants: [ - { - id: mockProductPriceUpdate.productVariantsBulkUpdate?.product - ?.variants.nodes[0].id, - price: product.price?.amount, - compareAtPrice: product.compareAtPrice?.amount, - }, - ], - }, - } - ); + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_PRICE_UPDATE, { + variables: { + id: mockProductPriceUpdate.productVariantsBulkUpdate?.product?.id, + variants: [ + { + id: mockProductPriceUpdate.productVariantsBulkUpdate?.product + ?.variants.nodes[0].id, + price: product.price?.amount, + compareAtPrice: product.compareAtPrice?.amount, + }, + ], + }, + }); }); }); diff --git a/src/functions/customer/orchestrations/product/update/update-price.ts b/src/functions/customer/orchestrations/product/update/update-price.ts index a4ae3082..b7fc6c9c 100644 --- a/src/functions/customer/orchestrations/product/update/update-price.ts +++ b/src/functions/customer/orchestrations/product/update/update-price.ts @@ -1,6 +1,7 @@ import { CustomerProductServiceGet } from "~/functions/customer/services/product/get"; import { shopifyAdmin } from "~/library/shopify"; +export const updatePriceName = "updatePrice"; export const updatePrice = async ({ customerId, productId, @@ -13,7 +14,7 @@ export const updatePrice = async ({ productId, }); - const { data } = await shopifyAdmin.request(PRODUCT_PRICE_UPDATE, { + const { data } = await shopifyAdmin().request(PRODUCT_PRICE_UPDATE, { variables: { id: `gid://shopify/Product/${productId}`, variants: [ 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 0c9ea14b..ebfc242d 100644 --- a/src/functions/customer/orchestrations/product/update/update-product.spec.ts +++ b/src/functions/customer/orchestrations/product/update/update-product.spec.ts @@ -14,13 +14,13 @@ import { PRODUCT_UPDATE, updateProduct } from "./update-product"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; const mockProductUpdate: ProductUpdateMutation = { productUpdate: { @@ -196,8 +196,8 @@ describe("CustomerProductUpdateOrchestration", () => { `city-${location.city.replace(/ /g, "-").toLowerCase()}`, ]; - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith(1, PRODUCT_UPDATE, { + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_UPDATE, { variables: { id: mockProductUpdate.productUpdate?.product?.id, title: product.title, diff --git a/src/functions/customer/orchestrations/product/update/update-product.ts b/src/functions/customer/orchestrations/product/update/update-product.ts index 529e3cf9..866963b4 100644 --- a/src/functions/customer/orchestrations/product/update/update-product.ts +++ b/src/functions/customer/orchestrations/product/update/update-product.ts @@ -5,6 +5,7 @@ import { LocationModel } from "~/functions/location"; import { NotFoundError } from "~/library/handler"; import { shopifyAdmin } from "~/library/shopify"; +export const updateProductName = "updateProduct"; export const updateProduct = async ({ customerId, productId, @@ -110,7 +111,7 @@ export const updateProduct = async ({ .join(", "), }; - const { data } = await shopifyAdmin.request(PRODUCT_UPDATE, { + const { data } = await shopifyAdmin().request(PRODUCT_UPDATE, { variables, }); diff --git a/src/functions/customer/services/location/create.spec.ts b/src/functions/customer/services/location/create.spec.ts index 93b64b97..4fdd8f10 100644 --- a/src/functions/customer/services/location/create.spec.ts +++ b/src/functions/customer/services/location/create.spec.ts @@ -19,13 +19,13 @@ jest.mock("~/functions/location/services/get-coordinates", () => ({ LocationServiceGetCoordinates: jest.fn(), })); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; type LocationServiceGetCoordinatesMock = jest.Mock< Promise<{ @@ -132,67 +132,63 @@ describe("CustomerLocationServiceCreate", () => { const response = await CustomerLocationServiceCreate(location); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - CREATE_LOCATION_METAOBJECT, - { - variables: ensureType({ - handle: response._id, - fields: [ - { - value: response.locationType, - key: "location_type", - }, - { - value: response.name, - key: "name", - }, - { - value: response.fullAddress, - key: "full_address", - }, - { - value: response.city, - key: "city", - }, - { - value: response.country, - key: "country", - }, - { - value: response.originType, - key: "origin_type", - }, - { - value: response.distanceForFree.toString(), - key: "distance_for_free", - }, - { - value: response.distanceHourlyRate.toString(), - key: "distance_hourly_rate", - }, - { - value: response.fixedRatePerKm.toString(), - key: "fixed_rate_per_km", - }, - { - value: response.minDriveDistance.toString(), - key: "min_drive_distance", - }, - { - value: response.maxDriveDistance.toString(), - key: "max_drive_distance", - }, - { - value: response.startFee.toString(), - key: "start_fee", - }, - ], - }), - } - ); + expect(mockRequest).toHaveBeenNthCalledWith(1, CREATE_LOCATION_METAOBJECT, { + variables: ensureType({ + handle: response._id, + fields: [ + { + value: response.locationType, + key: "location_type", + }, + { + value: response.name, + key: "name", + }, + { + value: response.fullAddress, + key: "full_address", + }, + { + value: response.city, + key: "city", + }, + { + value: response.country, + key: "country", + }, + { + value: response.originType, + key: "origin_type", + }, + { + value: response.distanceForFree.toString(), + key: "distance_for_free", + }, + { + value: response.distanceHourlyRate.toString(), + key: "distance_hourly_rate", + }, + { + value: response.fixedRatePerKm.toString(), + key: "fixed_rate_per_km", + }, + { + value: response.minDriveDistance.toString(), + key: "min_drive_distance", + }, + { + value: response.maxDriveDistance.toString(), + key: "max_drive_distance", + }, + { + value: response.startFee.toString(), + key: "start_fee", + }, + ], + }), + }); expect(omitObjectIdProps(response.toObject())).toEqual( expect.objectContaining({ diff --git a/src/functions/customer/services/location/create.ts b/src/functions/customer/services/location/create.ts index 0df7a05b..7aeab2b2 100644 --- a/src/functions/customer/services/location/create.ts +++ b/src/functions/customer/services/location/create.ts @@ -16,7 +16,7 @@ export const CustomerLocationServiceCreate = async ( location.country = result.country; const savedLocation = await location.save(); - const { data } = await shopifyAdmin.request(CREATE_LOCATION_METAOBJECT, { + const { data } = await shopifyAdmin().request(CREATE_LOCATION_METAOBJECT, { variables: { handle: location._id, fields: [ diff --git a/src/functions/customer/services/location/update.spec.ts b/src/functions/customer/services/location/update.spec.ts index 43387422..19dd7413 100644 --- a/src/functions/customer/services/location/update.spec.ts +++ b/src/functions/customer/services/location/update.spec.ts @@ -12,13 +12,12 @@ jest.mock("~/functions/location/services/get-coordinates", () => ({ LocationServiceGetCoordinates: jest.fn(), })); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); - -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; type LocationServiceGetCoordinatesMock = jest.Mock< Promise<{ diff --git a/src/functions/customer/services/location/update.ts b/src/functions/customer/services/location/update.ts index 2c8cc11e..cbbda48d 100644 --- a/src/functions/customer/services/location/update.ts +++ b/src/functions/customer/services/location/update.ts @@ -64,7 +64,7 @@ export const CustomerLocationServiceUpdate = async ( ); if (updateLocation.metafieldId) { - await shopifyAdmin.request(UPDATE_LOCATION_METAOBJECT, { + await shopifyAdmin().request(UPDATE_LOCATION_METAOBJECT, { variables: { id: updateLocation.metafieldId, fields: [ diff --git a/src/functions/customer/services/product-options/add.spec.ts b/src/functions/customer/services/product-options/add.spec.ts index 569e0d8b..db907aca 100644 --- a/src/functions/customer/services/product-options/add.spec.ts +++ b/src/functions/customer/services/product-options/add.spec.ts @@ -4,32 +4,25 @@ import { getProductObject } from "~/library/jest/helpers/product"; import { createScheduleWithProducts } from "~/library/jest/helpers/schedule"; import { shopifyAdmin } from "~/library/shopify"; import { GidFormat } from "~/library/zod"; -import { - ProductOptionAddMutation, - ProductOptionDuplicateMutation, - ProductParentUpdateMutation, -} from "~/types/admin.generated"; +import { ProductOptionDuplicateMutation } from "~/types/admin.generated"; import { CustomerProductOptionsServiceAdd, - PRODUCT_OPTION_ADD, PRODUCT_OPTION_DUPLCATE, - PRODUCT_PARENT_UPDATE, } from "./add"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerProductOptionsAddService", () => { beforeEach(() => { - // Clear all mocks before each test - (shopifyAdmin.request as jest.Mock).mockClear(); + jest.clearAllMocks(); }); it("should be able to add 1 or more options to a product", async () => { @@ -96,85 +89,9 @@ describe("CustomerProductOptionsAddService", () => { }, }; - const tags = [ - `user`, - `user-${user.username}`, - `userid-${customerId}`, - `options`, - `parentid-${product.productId}`, - `parent-${product.productHandle}`, - ]; - - const mockProductOptionUpdate: ProductOptionAddMutation = { - productUpdate: { - product: { - id: "gid://shopify/Product/9186772386119", - title: "New Product Title", - handle: "ikadsk", - tags, - required: { - id: "gid://shopify/Metafield/12", - value: "true", - }, - parentId: { - id: "gid://shopify/Metafield/44499605258567", - value: `gid://shopify/Product/${product.productId}`, - }, - variants: { - nodes: [ - { - id: "gid://shopify/ProductVariant/49475617128775", - title: "Tyk", - price: "12.00", - duration: { - id: "gid://shopify/Metafield/3", - value: "1", - }, - }, - { - id: "gid://shopify/ProductVariant/49475617259847", - title: "Normal", - price: "12.00", - duration: { - id: "gid://shopify/Metafield/2", - value: "2", - }, - }, - { - id: "gid://shopify/ProductVariant/49475617358151", - title: "Meget tyk", - price: "12.00", - duration: { - id: "gid://shopify/Metafield/1", - value: "3", - }, - }, - ], - }, - }, - }, - }; - - const mockProductParentUpdate: ProductParentUpdateMutation = { - productUpdate: { - product: { - options: { - id: "gid://shopify/Metafield/44505109102919", - }, - }, - }, - }; - - mockRequest - .mockResolvedValueOnce({ - data: mockProductOptionDuplicate, - }) - .mockResolvedValueOnce({ - data: mockProductOptionUpdate, - }) - .mockResolvedValueOnce({ - data: mockProductParentUpdate, - }); + mockRequest.mockResolvedValueOnce({ + data: mockProductOptionDuplicate, + }); const result = await CustomerProductOptionsServiceAdd({ customerId, @@ -184,61 +101,18 @@ describe("CustomerProductOptionsAddService", () => { }); //expect(result).toHaveLength(1); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(3); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - PRODUCT_OPTION_DUPLCATE, - { - variables: { - productId: `gid://shopify/Product/${cloneId}`, - title: - mockProductOptionDuplicate.productDuplicate?.newProduct?.title!, - }, - } - ); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 2, - PRODUCT_OPTION_ADD, - { - variables: { - id: mockProductOptionDuplicate.productDuplicate?.newProduct?.id, - metafields: [ - { - id: mockProductOptionDuplicate.productDuplicate?.newProduct - ?.parentId?.id, - value: `gid://shopify/Product/${product.productId}`, - }, - ], - tags: tags.join(", "), - }, - } - ); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 3, - PRODUCT_PARENT_UPDATE, - { - variables: { - id: `gid://shopify/Product/${product.productId}`, - metafields: [ - { - key: "options", - namespace: "booking", - value: JSON.stringify([ - mockProductOptionDuplicate.productDuplicate?.newProduct?.id, - ]), - }, - ], - }, - } - ); + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_OPTION_DUPLCATE, { + variables: { + productId: `gid://shopify/Product/${cloneId}`, + title: mockProductOptionDuplicate.productDuplicate?.newProduct?.title!, + }, + }); let schedule = await ScheduleModel.findOne(newSchedule._id).orFail(); expect(schedule).not.toBeNull(); expect(schedule.products).toHaveLength(1); let scheduleProduct = schedule.products[0]; - expect(scheduleProduct.optionsMetafieldId).toBe( - mockProductParentUpdate.productUpdate?.product?.options?.id - ); expect(scheduleProduct.options).toHaveLength(1); let options = scheduleProduct?.options![0]; expect(options.productId).toEqual( diff --git a/src/functions/customer/services/product-options/add.ts b/src/functions/customer/services/product-options/add.ts index 44281ecc..56d2858d 100644 --- a/src/functions/customer/services/product-options/add.ts +++ b/src/functions/customer/services/product-options/add.ts @@ -42,7 +42,7 @@ export async function CustomerProductOptionsServiceAdd({ productId, }); - const { data } = await shopifyAdmin.request(PRODUCT_OPTION_DUPLCATE, { + const { data } = await shopifyAdmin().request(PRODUCT_OPTION_DUPLCATE, { variables: { productId: `gid://shopify/Product/${cloneId}`, title, //Afrensning - XXX @@ -59,30 +59,9 @@ export async function CustomerProductOptionsServiceAdd({ ]); } - const newProductId = GidFormat.parse(data.productDuplicate.newProduct.id); - - await shopifyAdmin.request(PRODUCT_OPTION_ADD, { - variables: { - id: `gid://shopify/Product/${newProductId}`, - metafields: [ - { - id: data.productDuplicate.newProduct.parentId?.id, - value: `gid://shopify/Product/${productId}`, - }, - ], - tags: [ - `user`, - `user-${user.username}`, - `userid-${customerId}`, - `options`, - `parentid-${rootProduct.productId}`, - `parent-${rootProduct.productHandle}`, - ].join(", "), - }, - }); - const newOption: ScheduleProductOption = { - productId: newProductId, + parentIdMetafieldId: data.productDuplicate.newProduct.parentId?.id, + productId: GidFormat.parse(data.productDuplicate.newProduct.id), title: data.productDuplicate.newProduct.title, required: !data.productDuplicate.newProduct.required || @@ -99,42 +78,13 @@ export async function CustomerProductOptionsServiceAdd({ })) || [], }; - const options = mergeArraysUnique( - rootProduct?.options || [], - [newOption], - "productId" - ); - - const optionMetafield = rootProduct.optionsMetafieldId - ? { - id: rootProduct.optionsMetafieldId, - } - : { - key: "options", - namespace: "booking", - }; - - const { data: parentProductData } = await shopifyAdmin.request( - PRODUCT_PARENT_UPDATE, - { - variables: { - id: `gid://shopify/Product/${productId}`, - metafields: [ - { - ...optionMetafield, - value: JSON.stringify( - options.map((o) => `gid://shopify/Product/${o.productId}`) - ), - }, - ], - }, - } - ); - const newProduct: ScheduleProduct = { ...rootProduct, - optionsMetafieldId: parentProductData?.productUpdate?.product?.options?.id, - options, + options: mergeArraysUnique( + rootProduct?.options || [], + [newOption], + "productId" + ), }; await ScheduleModel.updateOne( @@ -190,26 +140,3 @@ export const PRODUCT_OPTION_DUPLCATE = `#graphql } } ` as const; - -export const PRODUCT_OPTION_ADD = `#graphql - ${PRODUCT_OPTION_FRAGMENT} - mutation ProductOptionAdd($id: ID!, $metafields: [MetafieldInput!]!, $tags: [String!]!) { - productUpdate(input: {id: $id, metafields: $metafields, tags: $tags}) { - product { - ...ProductOptionFragment - } - } - } -` as const; - -export const PRODUCT_PARENT_UPDATE = `#graphql - mutation ProductParentUpdate($id: ID, $metafields: [MetafieldInput!]) { - productUpdate(input: {id: $id, metafields: $metafields}) { - product { - options: metafield(key: "options", namespace: "booking") { - id - } - } - } - } -` as const; diff --git a/src/functions/customer/services/product-options/destroy.spec.ts b/src/functions/customer/services/product-options/destroy.spec.ts index 5834019d..17ab5cff 100644 --- a/src/functions/customer/services/product-options/destroy.spec.ts +++ b/src/functions/customer/services/product-options/destroy.spec.ts @@ -1,33 +1,11 @@ import { ScheduleModel } from "~/functions/schedule"; import { getProductObject } from "~/library/jest/helpers/product"; import { createScheduleWithProducts } from "~/library/jest/helpers/schedule"; -import { shopifyAdmin } from "~/library/shopify"; -import { - ProductDestroyMetafieldMutation, - ProductOptionDestroyMutation, -} from "~/types/admin.generated"; -import { - CustomerProductOptionsServiceDestroy, - PRODUCT_DESTROY_METAFIELD, - PRODUCT_OPTION_DESTROY, -} from "./destroy"; +import { CustomerProductOptionsServiceDestroy } from "./destroy"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ - request: jest.fn(), - }), -})); - -const mockRequest = shopifyAdmin.request as jest.Mock; - describe("CustomerProductOptionsDestroyService", () => { - beforeEach(() => { - // Clear all mocks before each test - (shopifyAdmin.request as jest.Mock).mockClear(); - }); - it("should be able to destroy product option", async () => { const productId = 321; const optionProductId = 123; @@ -36,20 +14,9 @@ describe("CustomerProductOptionsDestroyService", () => { id: "123", }; - const productDelete: ProductOptionDestroyMutation = { - productDelete: { deletedProductId: mockProduct.id }, - }; - - const metafieldDelete: ProductDestroyMetafieldMutation = { - metafieldDelete: { - deletedId: "gid://shopify/Metafield/44505109102919", - }, - }; - const product = { ...getProductObject({ productId: productId, - optionsMetafieldId: metafieldDelete.metafieldDelete?.deletedId!, options: [ { productId: optionProductId, @@ -67,15 +34,6 @@ describe("CustomerProductOptionsDestroyService", () => { products: [product], }); - // Setup mock responses - mockRequest - .mockResolvedValueOnce({ - data: productDelete, - }) - .mockResolvedValueOnce({ - data: metafieldDelete, - }); - const result = await CustomerProductOptionsServiceDestroy({ customerId, productId, @@ -84,30 +42,9 @@ describe("CustomerProductOptionsDestroyService", () => { expect(result).toHaveLength(0); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(2); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - PRODUCT_OPTION_DESTROY, - { - variables: { - productId: `gid://shopify/Product/${optionProductId}`, - }, - } - ); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 2, - PRODUCT_DESTROY_METAFIELD, - { - variables: { - metafieldId: metafieldDelete.metafieldDelete?.deletedId, - }, - } - ); - let schedule = await ScheduleModel.findOne(newSchedule._id).orFail(); expect(schedule.products).toHaveLength(1); let scheduleProduct = schedule.products[0]; expect(scheduleProduct.options).toHaveLength(0); - expect(scheduleProduct.optionsMetafieldId).toBe(null); }); }); diff --git a/src/functions/customer/services/product-options/destroy.ts b/src/functions/customer/services/product-options/destroy.ts index c94bffc8..fe9b538b 100644 --- a/src/functions/customer/services/product-options/destroy.ts +++ b/src/functions/customer/services/product-options/destroy.ts @@ -1,7 +1,5 @@ import { ScheduleModel } from "~/functions/schedule"; import { NotFoundError } from "~/library/handler"; -import { shopifyAdmin } from "~/library/shopify"; -import { PRODUCT_PARENT_UPDATE } from "./add"; export type CustomerProductOptionsDestroyProps = { customerId: number; @@ -14,12 +12,6 @@ export async function CustomerProductOptionsServiceDestroy({ optionProductId, productId, }: CustomerProductOptionsDestroyProps) { - await shopifyAdmin.request(PRODUCT_OPTION_DESTROY, { - variables: { - productId: `gid://shopify/Product/${optionProductId}`, - }, - }); - const schedule = await ScheduleModel.findOneAndUpdate( { customerId, @@ -39,7 +31,7 @@ export async function CustomerProductOptionsServiceDestroy({ new NotFoundError([ { path: ["customerId", "productId"], - message: "PRODUCT_NOT_FOUND", + message: "PRODUCT_OPTION_NOT_FOUND", code: "custom", }, ]) @@ -57,53 +49,5 @@ export async function CustomerProductOptionsServiceDestroy({ ]); } - if (product.options?.length === 0) { - await shopifyAdmin.request(PRODUCT_DESTROY_METAFIELD, { - variables: { - metafieldId: product.optionsMetafieldId || "", - }, - }); - - await ScheduleModel.updateOne( - { - customerId, - "products.productId": productId, - }, - { - $set: { - "products.$": { ...product, optionsMetafieldId: null }, - }, - } - ); - } else { - await shopifyAdmin.request(PRODUCT_PARENT_UPDATE, { - variables: { - id: `gid://shopify/Product/${productId}`, - metafields: [ - { - id: product.optionsMetafieldId, - value: JSON.stringify(product.options?.map((o) => o.productId)), - }, - ], - }, - }); - } - return product.options; } - -export const PRODUCT_OPTION_DESTROY = `#graphql - mutation productOptionDestroy($productId: ID!) { - productDelete(input: {id: $productId}) { - deletedProductId - } - } -` as const; - -export const PRODUCT_DESTROY_METAFIELD = `#graphql - mutation productDestroyMetafield($metafieldId: ID!){ - metafieldDelete(input: {id: $metafieldId}) { - deletedId - } - } -` as const; diff --git a/src/functions/customer/services/product-options/update.spec.ts b/src/functions/customer/services/product-options/update.spec.ts index 01ca88c6..f7f72f6e 100644 --- a/src/functions/customer/services/product-options/update.spec.ts +++ b/src/functions/customer/services/product-options/update.spec.ts @@ -11,18 +11,17 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerProductOptionsAddService", () => { beforeEach(() => { - // Clear all mocks before each test - (shopifyAdmin.request as jest.Mock).mockClear(); + jest.clearAllMocks(); }); it("should be able to add 1 or more options to a product", async () => { @@ -139,29 +138,25 @@ describe("CustomerProductOptionsAddService", () => { ); //expect(result).toHaveLength(1); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - PRODUCT_OPTION_UPDATE, - { - variables: { - productId: `gid://shopify/Product/${optionProductId}`, - variants: [ - { - id: `gid://shopify/ProductVariant/3`, - price: newDurationPrice.toString(), - metafields: [ - { - id: `gid://shopify/Metafield/33`, - value: newDurationValue.toString(), - type: "number_integer", - }, - ], - }, - ], - }, - } - ); + expect(mockRequest).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_OPTION_UPDATE, { + variables: { + productId: `gid://shopify/Product/${optionProductId}`, + variants: [ + { + id: `gid://shopify/ProductVariant/3`, + price: newDurationPrice.toString(), + metafields: [ + { + id: `gid://shopify/Metafield/33`, + value: newDurationValue.toString(), + type: "number_integer", + }, + ], + }, + ], + }, + }); let schedule = await ScheduleModel.findOne(newSchedule._id).orFail(); expect(schedule).not.toBeNull(); diff --git a/src/functions/customer/services/product-options/update.ts b/src/functions/customer/services/product-options/update.ts index 71ce5b13..c960e54e 100644 --- a/src/functions/customer/services/product-options/update.ts +++ b/src/functions/customer/services/product-options/update.ts @@ -67,7 +67,7 @@ export async function CustomerProductOptionsServiceUpdate( }; }); - const { data } = await shopifyAdmin.request(PRODUCT_OPTION_UPDATE, { + const { data } = await shopifyAdmin().request(PRODUCT_OPTION_UPDATE, { variables: { productId: `gid://shopify/Product/${props.optionProductId}`, variants, diff --git a/src/functions/customer/services/product/add.spec.ts b/src/functions/customer/services/product/add.spec.ts index 5752e3d7..10594dd6 100644 --- a/src/functions/customer/services/product/add.spec.ts +++ b/src/functions/customer/services/product/add.spec.ts @@ -14,13 +14,13 @@ import { CustomerProductServiceAdd, PRODUCT_DUPLCATE } from "./add"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerProductServiceAdd", () => { let mockProduct: ProductDuplicateMutation; @@ -61,7 +61,7 @@ describe("CustomerProductServiceAdd", () => { beforeEach(() => { // Clear all mocks before each test - (shopifyAdmin.request as jest.Mock).mockClear(); + jest.clearAllMocks(); mockProduct = { productDuplicate: { @@ -168,9 +168,9 @@ describe("CustomerProductServiceAdd", () => { } ); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith(1, PRODUCT_DUPLCATE, { + expect(mockRequest).toHaveBeenNthCalledWith(1, PRODUCT_DUPLCATE, { variables: { productId: `gid://shopify/Product/${productBody.parentId}`, title, diff --git a/src/functions/customer/services/product/add.ts b/src/functions/customer/services/product/add.ts index 5bb86fc0..60c1e71a 100644 --- a/src/functions/customer/services/product/add.ts +++ b/src/functions/customer/services/product/add.ts @@ -31,7 +31,7 @@ export const CustomerProductServiceAdd = async ( { customerId }: CustomerProductServiceAdd, { scheduleId, ...body }: CustomerProductServiceAddBody ) => { - const { data } = await shopifyAdmin.request(PRODUCT_DUPLCATE, { + const { data } = await shopifyAdmin().request(PRODUCT_DUPLCATE, { variables: { productId: `gid://shopify/Product/${body.parentId}`, title: body.title, diff --git a/src/functions/customer/services/product/destroy.spec.ts b/src/functions/customer/services/product/destroy.spec.ts index b7659023..4f0ba5b3 100644 --- a/src/functions/customer/services/product/destroy.spec.ts +++ b/src/functions/customer/services/product/destroy.spec.ts @@ -6,13 +6,13 @@ import { CustomerProductServiceDestroy } from "./destroy"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerProductServiceDestroy", () => { const customerId = 123; diff --git a/src/functions/customer/services/product/destroy.ts b/src/functions/customer/services/product/destroy.ts index 3f939952..6f621687 100644 --- a/src/functions/customer/services/product/destroy.ts +++ b/src/functions/customer/services/product/destroy.ts @@ -9,7 +9,7 @@ export type CustomerProductServiceDestroyFilter = { export const CustomerProductServiceDestroy = async ( filter: CustomerProductServiceDestroyFilter ) => { - await shopifyAdmin.request(PRODUCT_DESTROY, { + await shopifyAdmin().request(PRODUCT_DESTROY, { variables: { productId: `gid://shopify/Product/${filter.productId}`, }, diff --git a/src/functions/customer/services/schedule/create.spec.ts b/src/functions/customer/services/schedule/create.spec.ts index 5eef15be..7e073899 100644 --- a/src/functions/customer/services/schedule/create.spec.ts +++ b/src/functions/customer/services/schedule/create.spec.ts @@ -11,13 +11,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleServiceCreate", () => { const customerId = 123; @@ -60,37 +60,33 @@ describe("CustomerScheduleServiceCreate", () => { customerId, }); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - CREATE_SCHEDULE_METAOBJECT, - { - variables: ensureType({ - handle: newSchedule._id, - fields: [ - { - value: newSchedule.name, - key: "name", - }, - { - value: JSON.stringify([ - { - day: "monday", - intervals: [ - { - to: "16:00", - from: "08:00", - }, - ], - }, - ]), - key: "slots", - }, - ], - }), - } - ); + expect(mockRequest).toHaveBeenNthCalledWith(1, CREATE_SCHEDULE_METAOBJECT, { + variables: ensureType({ + handle: newSchedule._id, + fields: [ + { + value: newSchedule.name, + key: "name", + }, + { + value: JSON.stringify([ + { + day: "monday", + intervals: [ + { + to: "16:00", + from: "08:00", + }, + ], + }, + ]), + key: "slots", + }, + ], + }), + }); expect(newSchedule.name).toEqual(name); expect(newSchedule.metafieldId).toEqual( diff --git a/src/functions/customer/services/schedule/create.ts b/src/functions/customer/services/schedule/create.ts index 7caecc9a..a7965ce0 100644 --- a/src/functions/customer/services/schedule/create.ts +++ b/src/functions/customer/services/schedule/create.ts @@ -26,7 +26,7 @@ export const CustomerScheduleServiceCreate = async ( }); const scheduleModel = await newSchedule.save(); - const { data } = await shopifyAdmin.request(CREATE_SCHEDULE_METAOBJECT, { + const { data } = await shopifyAdmin().request(CREATE_SCHEDULE_METAOBJECT, { variables: { handle: scheduleModel._id, fields: [ diff --git a/src/functions/customer/services/schedule/destroy.spec.ts b/src/functions/customer/services/schedule/destroy.spec.ts index 51574b88..ffb46d00 100644 --- a/src/functions/customer/services/schedule/destroy.spec.ts +++ b/src/functions/customer/services/schedule/destroy.spec.ts @@ -6,13 +6,13 @@ import { CustomerScheduleServiceDestroy } from "./destroy"; require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleServiceDestroy", () => { const customerId = 123; diff --git a/src/functions/customer/services/schedule/destroy.ts b/src/functions/customer/services/schedule/destroy.ts index 8678a13e..20c9e215 100644 --- a/src/functions/customer/services/schedule/destroy.ts +++ b/src/functions/customer/services/schedule/destroy.ts @@ -15,7 +15,7 @@ export const CustomerScheduleServiceDestroy = async ( }); if (schedule && schedule.metafieldId) { - await shopifyAdmin.request(DESTROY_SCHEDULE_METAFIELD, { + await shopifyAdmin().request(DESTROY_SCHEDULE_METAFIELD, { variables: { metafieldId: schedule.metafieldId, }, diff --git a/src/functions/customer/services/schedule/update.spec.ts b/src/functions/customer/services/schedule/update.spec.ts index 34c13bf4..2b454470 100644 --- a/src/functions/customer/services/schedule/update.spec.ts +++ b/src/functions/customer/services/schedule/update.spec.ts @@ -13,13 +13,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleServiceUpdate", () => { const customerId = 123; @@ -85,23 +85,19 @@ describe("CustomerScheduleServiceUpdate", () => { } ); - expect(shopifyAdmin.request).toHaveBeenCalledTimes(1); + expect(mockRequest).toHaveBeenCalledTimes(1); - expect(shopifyAdmin.request).toHaveBeenNthCalledWith( - 1, - UPDATE_SCHEDULE_METAOBJECT, - { - variables: ensureType({ - id: newSchedule.metafieldId || "", - fields: [ - { - value: updatedScheduleName, - key: "name", - }, - ], - }), - } - ); + expect(mockRequest).toHaveBeenNthCalledWith(1, UPDATE_SCHEDULE_METAOBJECT, { + variables: ensureType({ + id: newSchedule.metafieldId || "", + fields: [ + { + value: updatedScheduleName, + key: "name", + }, + ], + }), + }); expect(updatedSchedule).toMatchObject({ name: updatedScheduleName, diff --git a/src/functions/customer/services/schedule/update.ts b/src/functions/customer/services/schedule/update.ts index af9b9eee..a7a3c77a 100644 --- a/src/functions/customer/services/schedule/update.ts +++ b/src/functions/customer/services/schedule/update.ts @@ -30,7 +30,7 @@ export const CustomerScheduleServiceUpdate = async ( ); if (updatedSchedule.metafieldId) { - await shopifyAdmin.request(UPDATE_SCHEDULE_METAOBJECT, { + await shopifyAdmin().request(UPDATE_SCHEDULE_METAOBJECT, { variables: { id: updatedSchedule.metafieldId, fields: [ diff --git a/src/functions/customer/services/slot.spec.ts b/src/functions/customer/services/slot.spec.ts index b59fae36..2bfbac90 100644 --- a/src/functions/customer/services/slot.spec.ts +++ b/src/functions/customer/services/slot.spec.ts @@ -10,13 +10,13 @@ import { require("~/library/jest/mongoose/mongodb.jest"); -jest.mock("@shopify/admin-api-client", () => ({ - createAdminApiClient: () => ({ +jest.mock("~/library/shopify", () => ({ + shopifyAdmin: jest.fn().mockReturnValue({ request: jest.fn(), }), })); -const mockRequest = shopifyAdmin.request as jest.Mock; +const mockRequest = shopifyAdmin().request as jest.Mock; describe("CustomerScheduleSlotService", () => { it("should update a slots", async () => { diff --git a/src/functions/customer/services/slots.ts b/src/functions/customer/services/slots.ts index 031aab94..e4799a66 100644 --- a/src/functions/customer/services/slots.ts +++ b/src/functions/customer/services/slots.ts @@ -30,7 +30,7 @@ export const CustomerScheduleSlotServiceUpdate = async ( const data = await schedule.updateSlots(updatedSlot); if (schedule.metafieldId) { - await shopifyAdmin.request(UPDATE_SCHEDULE_METAOBJECT, { + await shopifyAdmin().request(UPDATE_SCHEDULE_METAOBJECT, { variables: { id: schedule.metafieldId, fields: [ diff --git a/src/functions/schedule/schedule.types.ts b/src/functions/schedule/schedule.types.ts index 9468633d..367fddf0 100644 --- a/src/functions/schedule/schedule.types.ts +++ b/src/functions/schedule/schedule.types.ts @@ -59,6 +59,7 @@ const LocationZodSchema = z.object({ export type ScheduleProductLocation = z.infer; export const ScheduleProductOptionZodSchema = z.object({ + parentIdMetafieldId: z.string().optional(), productId: GidFormat, title: z.string(), required: z.boolean(), diff --git a/src/functions/schedule/schemas/product.schema.ts b/src/functions/schedule/schemas/product.schema.ts index 53e4718d..90b31700 100644 --- a/src/functions/schedule/schemas/product.schema.ts +++ b/src/functions/schedule/schemas/product.schema.ts @@ -12,6 +12,7 @@ import { export const OptionMongooseSchema = new mongoose.Schema( { + parentIdMetafieldId: String, productId: { type: Number, index: true, diff --git a/src/functions/user/services/user/filters.spec.ts b/src/functions/user/services/user/filters.spec.ts index a6c8b05f..583222f3 100644 --- a/src/functions/user/services/user/filters.spec.ts +++ b/src/functions/user/services/user/filters.spec.ts @@ -35,7 +35,7 @@ describe("UserServiceFilters", () => { { active: true, isBusiness: true, - professions: [Professions.LASH, Professions.MAKEUP_ARTIST], + professions: [Professions.LASH_TECHNICIAN, Professions.MAKEUP_ARTIST], specialties: pickMultipleItems(["a", "b", "c"], 2), } ); @@ -49,7 +49,7 @@ describe("UserServiceFilters", () => { } const results = await UserServiceFilters({ - profession: Professions.LASH, + profession: Professions.LASH_TECHNICIAN, }); const pickLocation = results.locations[0]; @@ -122,13 +122,13 @@ describe("UserServiceFilters", () => { { active: true, isBusiness: true, - professions: [Professions.LASH, Professions.MAKEUP_ARTIST], + professions: [Professions.LASH_TECHNICIAN, Professions.MAKEUP_ARTIST], } ); } const results = await UserServiceFilters({ - profession: Professions.LASH, + profession: Professions.LASH_TECHNICIAN, }); const resultDays = results.availableDays.map((result) => result.day); @@ -172,7 +172,7 @@ describe("UserServiceFilters", () => { } const results = await UserServiceFilters({ - profession: Professions.LASH, + profession: Professions.LASH_TECHNICIAN, }); expect(results).toBeDefined(); diff --git a/src/functions/user/services/user/top.spec.ts b/src/functions/user/services/user/top.spec.ts index 32aa4af8..04d02723 100644 --- a/src/functions/user/services/user/top.spec.ts +++ b/src/functions/user/services/user/top.spec.ts @@ -18,10 +18,10 @@ describe("UserServiceTop", () => { faker.helpers.arrayElement([ Professions.HAIR_STYLIST, Professions.ESTHETICIAN, - Professions.BROW, - Professions.LASH, - Professions.NAIL, - Professions.MASSAGE, + Professions.BROW_TECHNICIAN, + Professions.LASH_TECHNICIAN, + Professions.NAIL_TECHNICIAN, + Professions.MASSAGE_THERAPIST, Professions.MAKEUP_ARTIST, ]), ], diff --git a/src/functions/user/user.types.ts b/src/functions/user/user.types.ts index 48f6579d..95e93e24 100644 --- a/src/functions/user/user.types.ts +++ b/src/functions/user/user.types.ts @@ -4,11 +4,21 @@ import { GidFormat, NumberOrString } from "~/library/zod"; export enum Professions { MAKEUP_ARTIST = "makeup_artist", HAIR_STYLIST = "hair_stylist", - NAIL = "nail_technician", - LASH = "lash_technician", - BROW = "brow_technician", - MASSAGE = "massage_therapist", + NAIL_TECHNICIAN = "nail_technician", + LASH_TECHNICIAN = "lash_technician", + BROW_TECHNICIAN = "brow_technician", + MASSAGE_THERAPIST = "massage_therapist", ESTHETICIAN = "esthetician", + BARBER = "barber", + COSMETOLOGIST = "cosmetologist", + SPA_THERAPIST = "spa_therapist", + TATTOO_ARTIST = "tattoo_artist", + PIERCING_TECHNICIAN = "piercing_technician", + AROMATHERAPIST = "aromatherapist", + SKINCARE_SPECIALIST = "skincare_specialist", + HAIR_COLORIST = "hair_colorist", + BRIDAL_STYLIST = "bridal_stylist", + IMAGE_CONSULTANT = "image_consultant", } export enum Specialties { @@ -51,8 +61,8 @@ export const UserZodSchema = z.object({ isBusiness: z.boolean(), yearsExperience: NumberOrString.optional(), professions: z - .array(z.nativeEnum(Professions)) - .or(z.nativeEnum(Professions)) + .array(z.string()) + .or(z.string()) .transform((value) => (Array.isArray(value) ? value : [value])) .transform((array) => array.filter((value) => value.trim() !== "")) .transform((array) => [...new Set(array)]) diff --git a/src/functions/webhook-customer.function.ts b/src/functions/webhook-customer.function.ts index 82625aee..bb8bf90d 100644 --- a/src/functions/webhook-customer.function.ts +++ b/src/functions/webhook-customer.function.ts @@ -1,12 +1,17 @@ import "module-alias/register"; import { HttpRequest, InvocationContext, app } from "@azure/functions"; +import * as df from "durable-functions"; import { connect } from "~/library/mongoose"; import { BlockedModel } from "./blocked/blocked.model"; +import { CustomerUpdateOrchestration } from "./customer/orchestrations/customer/update"; +import { CustomerServiceGet } from "./customer/services/customer/get"; import { CustomerServiceUpdate } from "./customer/services/customer/update"; +import { CustomerProductsServiceListIds } from "./customer/services/product/list-ids"; import { LocationModel } from "./location"; import { ScheduleModel } from "./schedule"; import { UserModel } from "./user"; +import { ActivateAllProductsOrchestration } from "./webhook/customer/update"; export type Customer = { id: number; @@ -84,29 +89,42 @@ app.http("webhookCustomerUpdate", { methods: ["POST"], authLevel: "anonymous", route: "webhooks/customer/update", + extraInputs: [df.input.durableClient()], handler: async (request: HttpRequest, context: InvocationContext) => { await connect(); - const customer = (await request.json()) as unknown as Customer; - const active = customer.tags.includes("active"); - const customerId = customer.id; - await CustomerServiceUpdate( + const shopifyCustomer = (await request.json()) as unknown as Customer; + const active = shopifyCustomer.tags.includes("active"); + const customerId = shopifyCustomer.id; + + const customer = await CustomerServiceGet({ customerId }); + + const newCustomer = await CustomerServiceUpdate( { customerId }, { active, - email: customer.email, - fullname: `${customer.first_name} ${customer.last_name}`, - phone: customer.phone, + email: shopifyCustomer.email, + fullname: `${shopifyCustomer.first_name} ${shopifyCustomer.last_name}`, + phone: shopifyCustomer.phone, } ); + context.log( `Customer Update, customerId = '${customerId}', active = '${active}', updated` ); - /* - TODO: - should disable products? - should disable content metafields? - maybe use Shopify Flow? - */ + + if (customer.active !== active) { + await CustomerUpdateOrchestration(newCustomer, context); + + const productIds = await CustomerProductsServiceListIds({ + customerId, + }); + + await ActivateAllProductsOrchestration( + { customerId, productIds }, + context + ); + } + return { body: "" }; }, }); diff --git a/src/functions/webhook/customer/update.ts b/src/functions/webhook/customer/update.ts new file mode 100644 index 00000000..e41af734 --- /dev/null +++ b/src/functions/webhook/customer/update.ts @@ -0,0 +1,51 @@ +import { InvocationContext } from "@azure/functions"; +import { addSeconds } from "date-fns"; +import * as df from "durable-functions"; +import { OrchestrationContext } from "durable-functions"; +import { + updateProduct, + updateProductName, +} from "~/functions/customer/orchestrations/product/update/update-product"; +import { activityType } from "~/library/orchestration"; + +const orchestrator: df.OrchestrationHandler = function* ( + context: OrchestrationContext +) { + const input = context.df.getInput() as Input; + + for (const productId of input.productIds) { + yield context.df.callActivity( + updateProductName, + activityType({ + customerId: input.customerId, + productId, + }) + ); + + const nextExecution = addSeconds(context.df.currentUtcDateTime, 5); + yield context.df.createTimer(nextExecution); + } + + return { done: true }; +}; + +df.app.orchestration("ActivateAllProductsOrchestration", orchestrator); + +type Input = { customerId: number; productIds: number[] }; + +export const ActivateAllProductsOrchestration = async ( + input: Input, + context: InvocationContext +): Promise => { + const client = df.getClient(context); + const instanceId: string = await client.startNew( + "ActivateAllProductsOrchestration", + { + input, + } + ); + + context.log(`Started orchestration with ID = '${instanceId}'.`); + + return instanceId; +}; diff --git a/src/library/shopify/index.ts b/src/library/shopify/index.ts index d9859c5c..f116964a 100644 --- a/src/library/shopify/index.ts +++ b/src/library/shopify/index.ts @@ -3,8 +3,24 @@ import { createAdminApiClient } from "@shopify/admin-api-client"; /** * Create Shopify Admin client. */ -export const shopifyAdmin = createAdminApiClient({ - storeDomain: process.env["ShopifyStoreDomain"] || "", - accessToken: process.env["ShopifyApiAccessToken"] || "", - apiVersion: "2024-01", -}); +export const shopifyAdmin = () => { + const keys = [ + "ShopifyApiAccessToken", + "ShopifyApiAccessToken1", + "ShopifyApiAccessToken2", + ]; + + const getRandomKey = () => { + const randomIndex = Math.floor(Math.random() * keys.length); + return keys[randomIndex]; + }; + + // Get a random key + const randomKey = getRandomKey(); + + return createAdminApiClient({ + storeDomain: process.env["ShopifyStoreDomain"] || "", + accessToken: process.env[randomKey] || "", + apiVersion: "2024-01", + }); +}; diff --git a/src/library/shopify/rest.ts b/src/library/shopify/rest.ts index af321101..42e33194 100644 --- a/src/library/shopify/rest.ts +++ b/src/library/shopify/rest.ts @@ -1,7 +1,23 @@ import { createAdminRestApiClient } from "@shopify/admin-api-client"; -export const shopifyRest = createAdminRestApiClient({ - storeDomain: process.env["ShopifyStoreDomain"] || "", - accessToken: process.env["ShopifyApiAccessToken"] || "", - apiVersion: "2024-01", -}); +export const shopifyRest = () => { + const keys = [ + "ShopifyApiAccessToken", + "ShopifyApiAccessToken1", + "ShopifyApiAccessToken2", + ]; + + const getRandomKey = () => { + const randomIndex = Math.floor(Math.random() * keys.length); + return keys[randomIndex]; + }; + + // Get a random key + const randomKey = getRandomKey(); + + return createAdminRestApiClient({ + storeDomain: process.env["ShopifyStoreDomain"] || "", + accessToken: process.env[randomKey] || "", + apiVersion: "2024-01", + }); +};