diff --git a/.vscode/launch.json b/.vscode/launch.json index 1c6e50da..af09f317 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,6 +7,20 @@ "request": "attach", "port": 9229, "preLaunchTask": "func: host start" + }, + { + "type": "node", + "request": "launch", + "name": "Debug Jest Tests", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "${workspaceFolder}/src/functions/user/services/availability/get.spec.ts" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js" + } } ] -} \ No newline at end of file +} diff --git a/jest.config.js b/jest.config.js index 102f047a..1a2f86ac 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,4 +5,5 @@ module.exports = { moduleNameMapper: { "^~/(.*)$": "/src/$1", }, + setupFiles: ["./jest/load-env.js"], }; diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 5cad4c1e..396ddb50 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -236,6 +236,8 @@ components: $ref: paths/user/list/response.yaml UsersProfessionsResponse: $ref: paths/user/professions/response.yaml + UsersSpecialtiesResponse: + $ref: paths/user/specialties/response.yaml # UserAvailability UserAvailabilitySlot: @@ -326,6 +328,8 @@ paths: $ref: "./paths/user/schedule/get-by-location/index.yaml" /users/professions: $ref: "./paths/user/professions/index.yaml" + /users/specialties: + $ref: "./paths/user/specialties/index.yaml" /user/{username}/availability/{locationId}/generate: $ref: "paths/user/availability/generate/index.yaml" /user/{username}/availability/{locationId}/get: diff --git a/openapi/paths/user/list/index.yaml b/openapi/paths/user/list/index.yaml index 30e7747c..c08c48b4 100644 --- a/openapi/paths/user/list/index.yaml +++ b/openapi/paths/user/list/index.yaml @@ -11,9 +11,16 @@ parameters: type: string - name: profession in: query - description: profession like makeup_artist + description: profession schema: type: string + - name: specialties + in: query + description: specialties + schema: + type: array + items: + type: string - name: sortOrder in: query description: asc or desc diff --git a/openapi/paths/user/specialties/index.yaml b/openapi/paths/user/specialties/index.yaml new file mode 100644 index 00000000..df0c1918 --- /dev/null +++ b/openapi/paths/user/specialties/index.yaml @@ -0,0 +1,23 @@ +get: + tags: + - User + operationId: usersSpecialties + summary: GET Get all users specialties with total count + description: This endpoint get all users + responses: + "200": + description: "Response" + content: + application/json: + schema: + $ref: "./response.yaml" + "400": + $ref: "../../../responses/bad.yaml" + "401": + $ref: "../../../responses/unauthorized.yaml" + "403": + $ref: "../../../responses/forbidden.yaml" + "404": + $ref: "../../../responses/not-found.yaml" + + security: [] diff --git a/openapi/paths/user/specialties/response.yaml b/openapi/paths/user/specialties/response.yaml new file mode 100644 index 00000000..eb45a92d --- /dev/null +++ b/openapi/paths/user/specialties/response.yaml @@ -0,0 +1,13 @@ +type: object +properties: + success: + type: boolean + example: true + payload: + type: object + additionalProperties: + type: number + +required: + - success + - payload diff --git a/src/functions/user.function.ts b/src/functions/user.function.ts index 47789a75..ee8019d6 100644 --- a/src/functions/user.function.ts +++ b/src/functions/user.function.ts @@ -4,6 +4,7 @@ import { app } from "@azure/functions"; import { UserControllerGet } from "./user/controllers/user/get"; import { UserControllerList } from "./user/controllers/user/list"; import { UserControllerProfessions } from "./user/controllers/user/professions"; +import { UserControllerSpecialties } from "./user/controllers/user/specialties"; import { UserControllerUsernameTaken } from "./user/controllers/user/username-taken"; app.http("userGet", { @@ -33,3 +34,10 @@ app.http("usersProfessions", { route: "users/professions", handler: UserControllerProfessions, }); + +app.http("usersSpecialties", { + methods: ["GET"], + authLevel: "anonymous", + route: "users/specialties", + handler: UserControllerSpecialties, +}); diff --git a/src/functions/user/controllers/products/list-by-schedule.ts b/src/functions/user/controllers/products/list-by-schedule.ts index 0c5ad414..780399f1 100644 --- a/src/functions/user/controllers/products/list-by-schedule.ts +++ b/src/functions/user/controllers/products/list-by-schedule.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { _ } from "~/library/handler"; import { CustomerProductsServiceList } from "~/functions/customer/services/product/list"; -import { UserServiceGetCustomerId } from "~/functions/user"; +import { UserServiceGetCustomerId } from "../../services/user/get-customer-id"; export type UserProductsControllerListByScheduleRequest = { query: z.infer; diff --git a/src/functions/user/controllers/schedule/get-by-product.ts b/src/functions/user/controllers/schedule/get-by-product.ts index 8eda0944..a6405374 100644 --- a/src/functions/user/controllers/schedule/get-by-product.ts +++ b/src/functions/user/controllers/schedule/get-by-product.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { _ } from "~/library/handler"; import { UserScheduleServiceGetByProductId as UserScheduleServiceGetByProduct } from "../../services/schedule/get-by-product"; -import { UserServiceGetCustomerId } from "../../services/user"; +import { UserServiceGetCustomerId } from "../../services/user/get-customer-id"; export type UserScheduleControllerGetByProductRequest = { query: z.infer; diff --git a/src/functions/user/controllers/schedule/locations-list.ts b/src/functions/user/controllers/schedule/locations-list.ts index 0699b256..66a2e822 100644 --- a/src/functions/user/controllers/schedule/locations-list.ts +++ b/src/functions/user/controllers/schedule/locations-list.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { _ } from "~/library/handler"; import { UserScheduleServiceLocationsList } from "../../services/schedule/locations-list"; -import { UserServiceGet } from "../../services/user"; +import { UserServiceGet } from "../../services/user/get"; export type UserScheduleControllerLocationsListRequest = { query: z.infer; diff --git a/src/functions/user/controllers/user/get.ts b/src/functions/user/controllers/user/get.ts index 6b0287d6..def020d2 100644 --- a/src/functions/user/controllers/user/get.ts +++ b/src/functions/user/controllers/user/get.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { _ } from "~/library/handler"; - -import { UserServiceGet } from "../../services/user"; +import { UserServiceGet } from "../../services/user/get"; export type UserControllerGetRequest = { query: z.infer; diff --git a/src/functions/user/controllers/user/list.ts b/src/functions/user/controllers/user/list.ts index 0ef3c94f..7818a1be 100644 --- a/src/functions/user/controllers/user/list.ts +++ b/src/functions/user/controllers/user/list.ts @@ -2,7 +2,7 @@ import { _ } from "~/library/handler"; import { z } from "zod"; import { NumberOrStringType } from "~/library/zod"; -import { UserServiceList } from "../../services/user"; +import { UserServiceList } from "../../services/user/list"; export type UserControllerListRequest = { query: z.infer; @@ -17,6 +17,7 @@ export const UserControllerListSchema = z.object({ nextCursor: z.string().optional(), limit: NumberOrStringType.optional(), profession: z.string().optional(), + specialties: z.array(z.string()).optional(), sortOrder: z.nativeEnum(SortOrder).optional(), }); @@ -27,6 +28,6 @@ export type UserControllerListResponse = Awaited< export const UserControllerList = _( async ({ query }: UserControllerListRequest) => { const validateData = UserControllerListSchema.parse(query); - return await UserServiceList(validateData); + return UserServiceList(validateData); } ); diff --git a/src/functions/user/controllers/user/professions.ts b/src/functions/user/controllers/user/professions.ts index 45a84fd2..40cbc78f 100644 --- a/src/functions/user/controllers/user/professions.ts +++ b/src/functions/user/controllers/user/professions.ts @@ -1,6 +1,5 @@ import { _ } from "~/library/handler"; - -import { UserServiceProfessions } from "../../services/user"; +import { UserServiceProfessions } from "../../services/user/professions"; export type UserControllerProfessionsResponse = Awaited< ReturnType diff --git a/src/functions/user/controllers/user/specialties.ts b/src/functions/user/controllers/user/specialties.ts new file mode 100644 index 00000000..1d383ca2 --- /dev/null +++ b/src/functions/user/controllers/user/specialties.ts @@ -0,0 +1,23 @@ +import { _ } from "~/library/handler"; + +import { z } from "zod"; +import { UserServiceSpecialties } from "../../services/user/specialties"; + +export type UserControllerSpecialtiesRequest = { + query: z.infer; +}; + +export const UserControllerSpecialtiesSchema = z.object({ + profession: z.string(), +}); + +export type UserControllerSpecialtiesResponse = Awaited< + ReturnType +>; + +export const UserControllerSpecialties = _( + async ({ query }: UserControllerSpecialtiesRequest) => { + const validateData = UserControllerSpecialtiesSchema.parse(query); + return UserServiceSpecialties(validateData); + } +); diff --git a/src/functions/user/controllers/user/username-taken.ts b/src/functions/user/controllers/user/username-taken.ts index 3ed1d750..5cea8f33 100644 --- a/src/functions/user/controllers/user/username-taken.ts +++ b/src/functions/user/controllers/user/username-taken.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { _ } from "~/library/handler"; - -import { UserServiceUsernameTaken } from "../../services/user"; +import { UserServiceUsernameTaken } from "../../services/user/username-taken"; export type UserControllerUsernameTakenRequest = { query: z.infer; diff --git a/src/functions/user/index.ts b/src/functions/user/index.ts index 09295b44..ebb95f37 100644 --- a/src/functions/user/index.ts +++ b/src/functions/user/index.ts @@ -1,4 +1,3 @@ -export * from "./services/user"; export * from "./user.model"; export * from "./user.schema"; export * from "./user.types"; diff --git a/src/functions/user/services/availability/generate.ts b/src/functions/user/services/availability/generate.ts index a395b7b0..c29a552b 100644 --- a/src/functions/user/services/availability/generate.ts +++ b/src/functions/user/services/availability/generate.ts @@ -8,7 +8,8 @@ import { findStartAndEndDate } from "~/library/availability/find-start-end-date- import { generateAvailability } from "~/library/availability/generate-availability"; import { removeBookedSlots } from "~/library/availability/remove-booked-slots"; import { StringOrObjectId } from "~/library/zod"; -import { UserServiceGetCustomerId } from "../user"; + +import { UserServiceGetCustomerId } from "../user/get-customer-id"; import { UserAvailabilityServiceGetOrders } from "./get-orders"; export type UserAvailabilityServiceGenerateProps = { diff --git a/src/functions/user/services/location.ts b/src/functions/user/services/location.ts index 0703b2b5..3aa6f91f 100644 --- a/src/functions/user/services/location.ts +++ b/src/functions/user/services/location.ts @@ -1,7 +1,7 @@ import { LocationModel } from "~/functions/location"; import { NotFoundError } from "~/library/handler"; import { StringOrObjectId } from "~/library/zod"; -import { UserServiceGet } from "./user"; +import { UserServiceGet } from "./user/get"; export const UserLocationServiceGetOne = async ({ username, diff --git a/src/functions/user/services/products/get.ts b/src/functions/user/services/products/get.ts index 3ca08999..dc41326c 100644 --- a/src/functions/user/services/products/get.ts +++ b/src/functions/user/services/products/get.ts @@ -1,6 +1,6 @@ import { ScheduleModel } from "~/functions/schedule"; import { NotFoundError } from "~/library/handler"; -import { UserServiceGetCustomerId } from "../user"; +import { UserServiceGetCustomerId } from "../user/get-customer-id"; export type UserProductServiceGetFilter = { username: string; diff --git a/src/functions/user/services/schedule/get-by-location.ts b/src/functions/user/services/schedule/get-by-location.ts index 863b311a..0374f9f0 100644 --- a/src/functions/user/services/schedule/get-by-location.ts +++ b/src/functions/user/services/schedule/get-by-location.ts @@ -1,9 +1,10 @@ import mongoose from "mongoose"; import { ILocationDocument, Location } from "~/functions/location"; import { Schedule, ScheduleModel, ScheduleProduct } from "~/functions/schedule"; -import { UserServiceGetCustomerId } from "~/functions/user"; + import { NotFoundError } from "~/library/handler"; import { StringOrObjectId } from "~/library/zod"; +import { UserServiceGetCustomerId } from "../user/get-customer-id"; export type UserScheduleServiceGetByLocationProps = { username: string; diff --git a/src/functions/user/services/user.spec.ts b/src/functions/user/services/user.spec.ts deleted file mode 100644 index ecb7b06c..00000000 --- a/src/functions/user/services/user.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { faker } from "@faker-js/faker"; -import { CustomerServiceCreate } from "~/functions/customer/services/customer"; -import { createUser, getUserObject } from "~/library/jest/helpers"; -import { Professions } from "../user.types"; -import { - UserServiceGet, - UserServiceList, - UserServiceProfessions, - UserServiceUsernameTaken, -} from "./user"; - -require("~/library/jest/mongoose/mongodb.jest"); - -describe("UserService", () => { - it("check username is taken", async () => { - const userData = getUserObject(); - - await CustomerServiceCreate(userData); - - let response = await UserServiceUsernameTaken({ - username: userData.username, - }); - - expect(response.usernameTaken).toBeTruthy(); - - response = await UserServiceUsernameTaken({ - username: "testerne", - }); - - expect(response.usernameTaken).toBeFalsy(); - }); - - it("Should find user", async () => { - const username = faker.internet.userName(); - const userData = getUserObject({ - yearsExperience: 1, - professions: [Professions.MAKEUP_ARTIST], - username, - }); - - await CustomerServiceCreate(userData); - - const findUser = await UserServiceGet({ username }); - - expect(findUser.fullname).toEqual(userData.fullname); - }); - - it("Should get all users", async () => { - for (let customerId = 0; customerId < 25; customerId++) { - await createUser({ customerId }, { active: true, isBusiness: true }); - } - - const firstPage = await UserServiceList({ limit: 10 }); - expect(firstPage.results.length).toBe(10); - - const secondPage = await UserServiceList({ - nextCursor: firstPage.nextCursor, - limit: 10, - }); - expect(secondPage.results.length).toBe(10); - - // Check that the second page does not contain any users from the first page - const firstPageUserIds = new Set( - firstPage.results.map((user) => user._id.toString()) - ); - secondPage.results.forEach((user) => { - expect(firstPageUserIds.has(user._id.toString())).toBe(false); - }); - - const thirdPage = await UserServiceList({ - nextCursor: secondPage.nextCursor, - limit: 10, - }); - expect(thirdPage.results.length).toBe(5); - - const secondPageUserIds = new Set( - secondPage.results.map((user) => user._id.toString()) - ); - - thirdPage.results.forEach((user) => { - expect(secondPageUserIds.has(user._id.toString())).toBe(false); - }); - }); - - it("Should get group and count professions by all users", async () => { - const professions = Object.values(Professions); - - const professionCount = faker.number.int({ - min: 1, - max: professions.length, - }); - - for (let customerId = 0; customerId < 25; customerId++) { - const pickedProfessions = faker.helpers.arrayElements( - professions, - professionCount - ); - - await createUser( - { customerId }, - { active: true, isBusiness: true, professions: pickedProfessions } - ); - } - - const result = await UserServiceProfessions(); - for (const key in result) { - expect(typeof result[key]).toBe("number"); - } - }); -}); diff --git a/src/functions/user/services/user.ts b/src/functions/user/services/user.ts deleted file mode 100644 index 6fbdcacf..00000000 --- a/src/functions/user/services/user.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { FilterQuery } from "mongoose"; - -import { NotFoundError } from "~/library/handler"; -import { UserModel } from "../user.model"; -import { IUserDocument } from "../user.schema"; -import { User } from "../user.types"; - -export type UserServiceUsernameTakenProps = Required>; - -export const UserServiceUsernameTaken = async ({ - username, -}: UserServiceUsernameTakenProps) => { - const usernameTaken = await UserModel.findOne({ username }).lean(); - return { usernameTaken: !!usernameTaken }; -}; - -export type UserServiceGetCustomerIdProps = Required>; - -export const UserServiceGetCustomerId = ({ username }: UserServiceGetProps) => { - return UserModel.findOne( - { username, active: true, isBusiness: true }, - { customerId: 1 } - ) - .lean() - .orFail( - new NotFoundError([ - { - path: ["username"], - message: "NOT_FOUND", - code: "custom", - }, - ]) - ); -}; - -export type UserServiceGetProps = Required>; - -export const UserServiceGet = ({ username }: UserServiceGetProps) => { - return UserModel.findOne({ username, active: true, isBusiness: true }) - .lean() - .orFail( - new NotFoundError([ - { - path: ["username"], - message: "NOT_FOUND", - code: "custom", - }, - ]) - ); -}; - -type UserServiceListProps = { - nextCursor?: Date | string; - limit?: number; - profession?: string; - sortOrder?: "asc" | "desc"; -}; - -export const UserServiceList = async ({ - nextCursor, - limit, - profession, - sortOrder = "desc", -}: UserServiceListProps = {}) => { - let query: FilterQuery = { active: true, isBusiness: true }; - - if (profession) { - query = { - ...query, - professions: { $in: [profession] }, - }; - } - - const sortParam = sortOrder === "asc" ? 1 : -1; // 1 for 'asc', -1 for 'desc' - - if (nextCursor) { - query = { - ...query, - createdAt: sortParam === 1 ? { $gt: nextCursor } : { $lt: nextCursor }, - }; - } - - const l = limit || 10; - const users = await UserModel.find(query) - .sort({ createdAt: sortParam }) - .limit(l); - - return { - results: users, - nextCursor: - users.length >= l ? users[users.length - 1].createdAt : undefined, - }; -}; - -export const UserServiceProfessions = async () => { - const professionCount = await UserModel.aggregate([ - { $unwind: "$professions" }, - { - $group: { - _id: "$professions", - count: { $sum: 1 }, - }, - }, - ]); - - const professionCountFormatted: Record = {}; - for (const profession of professionCount) { - professionCountFormatted[profession._id] = profession.count; - } - - return professionCountFormatted; -}; - -export const UserServiceFindCustomerOrFail = ({ - customerId, -}: { - customerId: number; -}) => { - return UserModel.findOne({ - customerId, - }).orFail( - new NotFoundError([ - { - path: ["customerId"], - message: "NOT_FOUND", - code: "custom", - }, - ]) - ); -}; diff --git a/src/functions/user/services/user/find-customer.ts b/src/functions/user/services/user/find-customer.ts new file mode 100644 index 00000000..39cea261 --- /dev/null +++ b/src/functions/user/services/user/find-customer.ts @@ -0,0 +1,20 @@ +import { NotFoundError } from "~/library/handler"; +import { UserModel } from "../../user.model"; + +export const UserServiceFindCustomerOrFail = ({ + customerId, +}: { + customerId: number; +}) => { + return UserModel.findOne({ + customerId, + }).orFail( + new NotFoundError([ + { + path: ["customerId"], + message: "NOT_FOUND", + code: "custom", + }, + ]) + ); +}; diff --git a/src/functions/user/services/user/get-customer-id.ts b/src/functions/user/services/user/get-customer-id.ts new file mode 100644 index 00000000..ce3ba727 --- /dev/null +++ b/src/functions/user/services/user/get-customer-id.ts @@ -0,0 +1,24 @@ +import { NotFoundError } from "~/library/handler"; +import { UserModel } from "../../user.model"; +import { User } from "../../user.types"; + +export type UserServiceGetCustomerIdProps = Required>; + +export const UserServiceGetCustomerId = ({ + username, +}: UserServiceGetCustomerIdProps) => { + return UserModel.findOne( + { username, active: true, isBusiness: true }, + { customerId: 1 } + ) + .lean() + .orFail( + new NotFoundError([ + { + path: ["username"], + message: "NOT_FOUND", + code: "custom", + }, + ]) + ); +}; diff --git a/src/functions/user/services/user/get.spec.ts b/src/functions/user/services/user/get.spec.ts new file mode 100644 index 00000000..d1c7ab71 --- /dev/null +++ b/src/functions/user/services/user/get.spec.ts @@ -0,0 +1,24 @@ +import { faker } from "@faker-js/faker"; +import { CustomerServiceCreate } from "~/functions/customer/services/customer"; +import { getUserObject } from "~/library/jest/helpers"; +import { Professions } from "../../user.types"; +import { UserServiceGet } from "./get"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("UserServiceGet", () => { + it("Should find user", async () => { + const username = faker.internet.userName(); + const userData = getUserObject({ + yearsExperience: 1, + professions: [Professions.MAKEUP_ARTIST], + username, + }); + + await CustomerServiceCreate(userData); + + const findUser = await UserServiceGet({ username }); + + expect(findUser.fullname).toEqual(userData.fullname); + }); +}); diff --git a/src/functions/user/services/user/get.ts b/src/functions/user/services/user/get.ts new file mode 100644 index 00000000..a7cc835d --- /dev/null +++ b/src/functions/user/services/user/get.ts @@ -0,0 +1,19 @@ +import { NotFoundError } from "~/library/handler"; +import { UserModel } from "../../user.model"; +import { User } from "../../user.types"; + +export type UserServiceGetProps = Required>; + +export const UserServiceGet = ({ username }: UserServiceGetProps) => { + return UserModel.findOne({ username, active: true, isBusiness: true }) + .lean() + .orFail( + new NotFoundError([ + { + path: ["username"], + message: "NOT_FOUND", + code: "custom", + }, + ]) + ); +}; diff --git a/src/functions/user/services/user/list.spec.ts b/src/functions/user/services/user/list.spec.ts new file mode 100644 index 00000000..f62c1f11 --- /dev/null +++ b/src/functions/user/services/user/list.spec.ts @@ -0,0 +1,56 @@ +import { faker } from "@faker-js/faker"; +import { createUser } from "~/library/jest/helpers"; +import { Professions } from "../../user.types"; +import { UserServiceList } from "./list"; +import { UserServiceProfessions } from "./professions"; +import { UserServiceSpecialties } from "./specialties"; + +require("~/library/jest/mongoose/mongodb.jest"); + +export const pickMultipleItems = (array: string[], count: number) => { + const shuffled = array.sort(() => 0.5 - Math.random()); + return shuffled.slice(0, count); +}; + +describe("UserServiceList", () => { + it("Should get all users", async () => { + for (let customerId = 0; customerId < 25; customerId++) { + await createUser( + { customerId }, + { + active: true, + isBusiness: true, + professions: [ + faker.helpers.arrayElement([ + Professions.HAIR_STYLIST, + Professions.ESTHETICIAN, + ]), + ], + specialties: pickMultipleItems(["a", "b", "c"], 2), + } + ); + } + + const firstPage = await UserServiceList({ limit: 10 }); + expect(firstPage.results.length).toBe(10); + expect(firstPage.total).toBe(25); + + const professions = await UserServiceProfessions(); + for (const profession in professions) { + const result = await UserServiceList({ limit: 10, profession }); + expect(result.total).toBe(professions[profession]); + + const specialties = await UserServiceSpecialties({ + profession, + }); + for (const specialty in specialties) { + const result = await UserServiceList({ + limit: 10, + profession, + specialties: [specialty], + }); + expect(result.total).toBe(specialties[specialty]); + } + } + }); +}); diff --git a/src/functions/user/services/user/list.ts b/src/functions/user/services/user/list.ts new file mode 100644 index 00000000..505a8c08 --- /dev/null +++ b/src/functions/user/services/user/list.ts @@ -0,0 +1,55 @@ +import { FilterQuery } from "mongoose"; +import { UserModel } from "../../user.model"; +import { IUserDocument } from "../../user.schema"; + +export type UserServiceListProps = { + nextCursor?: Date | string; + limit?: number; + profession?: string; + specialties?: string[]; + sortOrder?: "asc" | "desc"; +}; + +export const UserServiceList = async ({ + nextCursor, + limit, + profession, + specialties, + sortOrder = "desc", +}: UserServiceListProps = {}) => { + let query: FilterQuery = { active: true, isBusiness: true }; + + if (profession) { + query = { + ...query, + professions: { $in: [profession] }, + }; + } + + // Filter by specialties, if provided + if (specialties && specialties.length > 0) { + query = { ...query, specialties: { $all: specialties } }; + } + + const sortParam = sortOrder === "asc" ? 1 : -1; // 1 for 'asc', -1 for 'desc' + + if (nextCursor) { + query = { + ...query, + createdAt: sortParam === 1 ? { $gt: nextCursor } : { $lt: nextCursor }, + }; + } + + const totalCount = await UserModel.countDocuments(query); + const l = limit || 10; + const users = await UserModel.find(query) + .sort({ createdAt: sortParam }) + .limit(l); + + return { + results: users, + nextCursor: + users.length >= l ? users[users.length - 1].createdAt : undefined, + total: totalCount, + }; +}; diff --git a/src/functions/user/services/user/professions.spec.ts b/src/functions/user/services/user/professions.spec.ts new file mode 100644 index 00000000..84538603 --- /dev/null +++ b/src/functions/user/services/user/professions.spec.ts @@ -0,0 +1,34 @@ +import { faker } from "@faker-js/faker"; +import { createUser } from "~/library/jest/helpers"; +import { Professions } from "../../user.types"; +import { UserServiceProfessions } from "./professions"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("UserServiceProfessions", () => { + it("Should get group and count professions by all users", async () => { + const professions = Object.values(Professions); + + const professionCount = faker.number.int({ + min: 1, + max: professions.length, + }); + + for (let customerId = 0; customerId < 25; customerId++) { + const pickedProfessions = faker.helpers.arrayElements( + professions, + professionCount + ); + + await createUser( + { customerId }, + { active: true, isBusiness: true, professions: pickedProfessions } + ); + } + + const result = await UserServiceProfessions(); + for (const key in result) { + expect(typeof result[key]).toBe("number"); + } + }); +}); diff --git a/src/functions/user/services/user/professions.ts b/src/functions/user/services/user/professions.ts new file mode 100644 index 00000000..51c18bf5 --- /dev/null +++ b/src/functions/user/services/user/professions.ts @@ -0,0 +1,20 @@ +import { UserModel } from "../../user.model"; + +export const UserServiceProfessions = async () => { + const professionCount = await UserModel.aggregate([ + { $unwind: "$professions" }, + { + $group: { + _id: "$professions", + count: { $sum: 1 }, + }, + }, + ]); + + const professionCountFormatted: Record = {}; + for (const profession of professionCount) { + professionCountFormatted[profession._id] = profession.count; + } + + return professionCountFormatted; +}; diff --git a/src/functions/user/services/user/specialties.spec.ts b/src/functions/user/services/user/specialties.spec.ts new file mode 100644 index 00000000..b9ef4aae --- /dev/null +++ b/src/functions/user/services/user/specialties.spec.ts @@ -0,0 +1,42 @@ +import { createUser } from "~/library/jest/helpers"; +import { Professions } from "../../user.types"; +import { UserServiceList } from "./list"; +import { pickMultipleItems } from "./list.spec"; +import { UserServiceSpecialties } from "./specialties"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("UserServiceSpecialties", () => { + it("Should get group and count specialties by all users", async () => { + for (let customerId = 0; customerId < 25; customerId++) { + await createUser( + { customerId }, + { + active: true, + isBusiness: true, + professions: [Professions.MAKEUP_ARTIST], + specialties: pickMultipleItems(["a", "b", "c"], 2), + } + ); + } + + const result = await UserServiceSpecialties({ + profession: Professions.MAKEUP_ARTIST, + }); + for (const key in result) { + expect(typeof result[key]).toBe("number"); + } + + const users = await UserServiceList({ + limit: 5, + profession: Professions.MAKEUP_ARTIST, + specialties: ["a"], + }); + + const allHaveSpecialtyA = users.results.every( + (user) => user.specialties && user.specialties.includes("a") + ); + + expect(allHaveSpecialtyA).toBeTruthy(); + }); +}); diff --git a/src/functions/user/services/user/specialties.ts b/src/functions/user/services/user/specialties.ts new file mode 100644 index 00000000..899a054f --- /dev/null +++ b/src/functions/user/services/user/specialties.ts @@ -0,0 +1,26 @@ +import { UserModel } from "../../user.model"; + +export const UserServiceSpecialties = async ({ + profession, +}: { + profession: string; +}) => { + const professionCount = await UserModel.aggregate([ + { $unwind: "$professions" }, + { $match: { professions: profession } }, + { $unwind: "$specialties" }, + { + $group: { + _id: "$specialties", + count: { $sum: 1 }, + }, + }, + ]); + + const professionCountFormatted: Record = {}; + for (const profession of professionCount) { + professionCountFormatted[profession._id] = profession.count; + } + + return professionCountFormatted; +}; diff --git a/src/functions/user/services/user/username-taken.spec.ts b/src/functions/user/services/user/username-taken.spec.ts new file mode 100644 index 00000000..b3aab6ed --- /dev/null +++ b/src/functions/user/services/user/username-taken.spec.ts @@ -0,0 +1,25 @@ +import { CustomerServiceCreate } from "~/functions/customer/services/customer"; +import { getUserObject } from "~/library/jest/helpers"; +import { UserServiceUsernameTaken } from "./username-taken"; + +require("~/library/jest/mongoose/mongodb.jest"); + +describe("UserServiceUsernameTaken", () => { + it("check username is taken", async () => { + const userData = getUserObject(); + + await CustomerServiceCreate(userData); + + let response = await UserServiceUsernameTaken({ + username: userData.username, + }); + + expect(response.usernameTaken).toBeTruthy(); + + response = await UserServiceUsernameTaken({ + username: "testerne", + }); + + expect(response.usernameTaken).toBeFalsy(); + }); +}); diff --git a/src/functions/user/services/user/username-taken.ts b/src/functions/user/services/user/username-taken.ts new file mode 100644 index 00000000..4ce0ca3f --- /dev/null +++ b/src/functions/user/services/user/username-taken.ts @@ -0,0 +1,11 @@ +import { UserModel } from "../../user.model"; +import { User } from "../../user.types"; + +export type UserServiceUsernameTakenProps = Required>; + +export const UserServiceUsernameTaken = async ({ + username, +}: UserServiceUsernameTakenProps) => { + const usernameTaken = await UserModel.findOne({ username }).lean(); + return { usernameTaken: !!usernameTaken }; +}; diff --git a/src/functions/webhook.function.ts b/src/functions/webhook.function.ts new file mode 100644 index 00000000..fac1d23f --- /dev/null +++ b/src/functions/webhook.function.ts @@ -0,0 +1,17 @@ +import "module-alias/register"; + +import { HttpRequest, InvocationContext, app } from "@azure/functions"; + +/* + * just for testing webhooks + */ + +app.http("webhook", { + methods: ["POST"], + authLevel: "anonymous", + route: "webhooks", + handler: async (request: HttpRequest, context: InvocationContext) => { + console.log(await request.json()); + return { body: "test" }; + }, +}); diff --git a/src/library/availability/start-end-date.spec.ts b/src/library/availability/start-end-date.spec.ts index 121ef332..808ac67b 100644 --- a/src/library/availability/start-end-date.spec.ts +++ b/src/library/availability/start-end-date.spec.ts @@ -1,6 +1,5 @@ import { add, endOfMonth, isSameDay } from "date-fns"; import { - Schedule, ScheduleProductBookingPeriod, ScheduleProductNoticePeriod, TimeUnit, @@ -43,31 +42,39 @@ describe("generateStartDate", () => { }); }); +const OriginalDate = Date; describe("generateEndDate", () => { + beforeAll(() => { + jest + .spyOn(global, "Date") + .mockImplementation(() => new OriginalDate("2023-01-01T00:00:00Z")); + }); + it("should return the booking period end date when it is more than the minimum availability period", () => { - const startDate = new Date(); + const startDate = new OriginalDate("2023-02-02T00:00:00Z"); const bookingPeriod: ScheduleProductBookingPeriod = { unit: TimeUnit.WEEKS, value: 2, }; - const schedule: Pick = { - slots: [ - { day: "monday", intervals: [] }, - { day: "tuesday", intervals: [] }, - ], - products: [], - }; + const result = generateEndDate({ startDate, bookingPeriod, }); + const expected = add(startDate, { [bookingPeriod.unit]: bookingPeriod.value, }); expect(isSameDay(result, expected)).toBe(true); }); + afterAll(() => { + // Restore the original implementation + jest.restoreAllMocks(); + }); +}); +describe("generateEndDate", () => { it("should limit availability to end of month when booking period is larger", () => { const startDate = new Date(); const bookingPeriod: ScheduleProductBookingPeriod = { @@ -75,10 +82,6 @@ describe("generateEndDate", () => { value: 4, // large booking period }; - const schedule: Pick = { - slots: [{ day: "monday", intervals: [] }], // only one slot per week - products: [], - }; const result = generateEndDate({ startDate, bookingPeriod, diff --git a/src/library/availability/start-end-date.ts b/src/library/availability/start-end-date.ts index 18a4d469..19464fde 100644 --- a/src/library/availability/start-end-date.ts +++ b/src/library/availability/start-end-date.ts @@ -33,15 +33,16 @@ export type generateEndDateProps = { bookingPeriod: ScheduleProductBookingPeriod; }; -// Function to generate the end date export const generateEndDate = ({ startDate, bookingPeriod, }: generateEndDateProps) => { - const end = add(new Date(), { [bookingPeriod.unit]: bookingPeriod.value }); + const endDateAllowed = add(new Date(), { + [bookingPeriod.unit]: bookingPeriod.value, + }); const endMonth = endOfMonth(startDate); - if (isAfter(endMonth, end)) { - return end; + if (isAfter(endMonth, endDateAllowed)) { + return endDateAllowed; } return endMonth; };