Skip to content

Commit

Permalink
✨ feat(products): find merchants, minor event fixes (#567)
Browse files Browse the repository at this point in the history
  • Loading branch information
larwaa authored Mar 22, 2024
1 parent 46d1472 commit 8b22b25
Show file tree
Hide file tree
Showing 17 changed files with 351 additions and 50 deletions.
6 changes: 6 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,11 @@ type Merchant {
name: String!
}

type MerchantsResponse {
merchants: [Merchant!]!
total: Int!
}

type Mutation {
"""Add a member to the organization"""
addMember(data: AddMemberInput!): AddMemberResponse!
Expand Down Expand Up @@ -1161,6 +1166,7 @@ type Query {
hasRole(data: HasRoleInput!): HasRoleResponse!
listing(data: ListingInput!): ListingResponse!
listings: ListingsResponse!
merchants: MerchantsResponse!

"""Get an order by its ID."""
order(data: OrderInput!): OrderResponse!
Expand Down
20 changes: 13 additions & 7 deletions src/domain/events/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ type NewBasicEventData = {
contactEmail?: string | null;
location?: string | null;
organizationId: string;
categories?: { id: string }[];
categories?: { id: string }[] | null;
};

type NewSignUpEventData = NewBasicEventData & {
Expand Down Expand Up @@ -212,18 +212,24 @@ function newBasicEvent(basicEvent: NewBasicEventData): NewBasicEventReturn {
shortDescription: z.string().max(200).default(""),
startAt: z.date().min(new Date()),
endAt: z.date().min(new Date()),
contactEmail: z
.string()
.email()
.nullish()
.transform((val) => val ?? ""),
contactEmail: z.union([
z.literal(""),
z
.string()
.email()
.nullish()
.transform((val) => val ?? ""),
]),
location: z.string().default(""),
organizationId: z.string().uuid(),
signUpsEnabled: z
.boolean()
.nullish()
.transform((val) => val ?? false),
categories: z.array(z.object({ id: z.string().uuid() })).optional(),
categories: z
.array(z.object({ id: z.string().uuid() }))
.nullish()
.transform((val) => val ?? []),
signUpsRetractable: z
.boolean()
.nullish()
Expand Down
5 changes: 4 additions & 1 deletion src/graphql/events/resolvers/Mutation/createEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import type { MutationResolvers } from "./../../../types.generated.js";

export const createEvent: NonNullable<MutationResolvers["createEvent"]> =
async (_parent, { data }, ctx) => {
const { type, signUpDetails, ...event } = data.event;
const { type, signUpDetails, categories, ...event } = data.event;
const { tickets } = signUpDetails ?? {};

let createEventResult: Awaited<ReturnType<typeof ctx.events.create>>;
if (type === "BASIC") {
createEventResult = await ctx.events.create(ctx, {
type: "BASIC",
event: event,
categories,
});
} else if (!signUpDetails) {
throw new GraphQLError(
Expand All @@ -27,6 +28,7 @@ export const createEvent: NonNullable<MutationResolvers["createEvent"]> =
signUpsEndAt: signUpDetails.signUpsEndAt,
signUpsStartAt: signUpDetails.signUpsStartAt,
},
categories,
slots: signUpDetails.slots,
});
} else if (!tickets) {
Expand All @@ -43,6 +45,7 @@ export const createEvent: NonNullable<MutationResolvers["createEvent"]> =
signUpsEndAt: signUpDetails.signUpsEndAt,
signUpsStartAt: signUpDetails.signUpsStartAt,
},
categories,
slots: signUpDetails.slots,
tickets: tickets,
});
Expand Down
4 changes: 4 additions & 0 deletions src/graphql/products/resolvers/MerchantsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { MerchantsResponseResolvers } from "./../../types.generated.js";
export const MerchantsResponse: MerchantsResponseResolvers = {
/* Implement MerchantsResponse resolver logic here */
};
12 changes: 12 additions & 0 deletions src/graphql/products/resolvers/Query/merchants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { QueryResolvers } from "./../../../types.generated.js";
export const merchants: NonNullable<QueryResolvers["merchants"]> = async (
_parent,
_arg,
ctx,
) => {
const findManyMerchantsResult = await ctx.products.merchants.findMany(ctx);
if (!findManyMerchantsResult.ok) {
throw findManyMerchantsResult.error;
}
return findManyMerchantsResult.data;
};
69 changes: 69 additions & 0 deletions src/graphql/products/resolvers/Query/merchants.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { UnauthorizedError, errorCodes } from "~/domain/errors.js";
import { createMockApolloServer } from "~/graphql/test-clients/mock-apollo-server.js";
import { graphql } from "~/graphql/test-clients/unit/gql.js";
import { Result } from "~/lib/result.js";

describe("Product queries", () => {
it("returns all merchants", async () => {
const { client, productService } = createMockApolloServer();

productService.merchants.findMany.mockResolvedValue(
Result.success({
merchants: [],
total: 0,
}),
);

const { data } = await client.query({
query: graphql(`
query Merchants {
merchants {
merchants {
id
}
total
}
}
`),
});

expect(data).toEqual({
merchants: {
merchants: [],
total: 0,
},
});
});

it("throws if it encounters an error", async () => {
const { client, productService } = createMockApolloServer();

productService.merchants.findMany.mockResolvedValue(
Result.error(new UnauthorizedError("")),
);

const { errors } = await client.query({
query: graphql(`
query Merchants {
merchants {
merchants {
id
}
total
}
}
`),
});

expect(errors).toBeDefined();
expect(errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
extensions: expect.objectContaining({
code: errorCodes.ERR_UNAUTHORIZED,
}),
}),
]),
);
});
});
80 changes: 44 additions & 36 deletions src/graphql/products/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
type Mutation {
"""
Initiates a payment attempt for the given order.
"""
initiatePaymentAttempt(
data: InitiatePaymentAttemptInput!
): InitiatePaymentAttemptResponse!
"""
Creates an order for the given product.
"""
createOrder(data: CreateOrderInput!): CreateOrderResponse!
"""
Create a new Vipps merchant, and return the created merchant.
Requires super user status.
"""
createMerchant(data: CreateMerchantInput!): CreateMerchantResponse!
}

type Query {
"""
Get an order by its ID.
"""
order(data: OrderInput!): OrderResponse!

products: ProductResponse!
"""
Get orders, filtered by the given input. Unless the user is a super user, only
orders for the current user will be returned.
"""
orders(data: OrdersInput): OrdersResponse!
"""
Get payment attempts, filtered by the given input. Unless the user is a super user, only
payment attempts for the current user will be returned.
"""
paymentAttempts(data: PaymentAttemptsInput): PaymentAttemptsResponse!
merchants: MerchantsResponse!
}


type MerchantsResponse {
merchants: [Merchant!]!
total: Int!
}

input InitiatePaymentAttemptInput {
"""
The ID of the order to initiate a payment attempt for.
Expand Down Expand Up @@ -245,39 +289,3 @@ type OrderResponse {
order: Order!
}

type Mutation {
"""
Initiates a payment attempt for the given order.
"""
initiatePaymentAttempt(
data: InitiatePaymentAttemptInput!
): InitiatePaymentAttemptResponse!
"""
Creates an order for the given product.
"""
createOrder(data: CreateOrderInput!): CreateOrderResponse!
"""
Create a new Vipps merchant, and return the created merchant.
Requires super user status.
"""
createMerchant(data: CreateMerchantInput!): CreateMerchantResponse!
}

type Query {
"""
Get an order by its ID.
"""
order(data: OrderInput!): OrderResponse!

products: ProductResponse!
"""
Get orders, filtered by the given input. Unless the user is a super user, only
orders for the current user will be returned.
"""
orders(data: OrdersInput): OrdersResponse!
"""
Get payment attempts, filtered by the given input. Unless the user is a super user, only
payment attempts for the current user will be returned.
"""
paymentAttempts(data: PaymentAttemptsInput): PaymentAttemptsResponse!
}
7 changes: 7 additions & 0 deletions src/lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,12 @@ type IProductService = {
| InvalidArgumentError
| InternalServerError
>;
findMany(
ctx: Context,
): ResultAsync<
{ merchants: MerchantType[]; total: number },
UnauthorizedError | InternalServerError
>;
};
};

Expand Down Expand Up @@ -888,4 +894,5 @@ export type {
IUserService,
NewBookingParams,
Services,
IProductService,
};
19 changes: 19 additions & 0 deletions src/repositories/products/__tests__/integration/merchant.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import assert, { fail } from "node:assert";
import { faker } from "@faker-js/faker";
import { InvalidArgumentError, NotFoundError } from "~/domain/errors.js";
import { makeMockContext } from "~/lib/context.js";
import { Result } from "~/lib/result.js";
import { makeDependencies } from "./dependencies.js";

describe("productRepository", () => {
Expand Down Expand Up @@ -131,5 +133,22 @@ describe("productRepository", () => {
expect(actual.error).toBeInstanceOf(NotFoundError);
});
});

describe("#findManyMerchants", () => {
it("returns merchants and the total count", async () => {
const { productRepository, merchant } = await makeDependencies();

const actual = await productRepository.findManyMerchants(
makeMockContext(),
);

expect(actual).toEqual(
Result.success({
merchants: expect.arrayContaining([merchant]),
total: expect.any(Number),
}),
);
});
});
});
});
34 changes: 32 additions & 2 deletions src/repositories/products/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,42 @@ import {
type PaymentAttemptType,
type ProductType,
} from "~/domain/products.js";
import type { Context } from "~/lib/context.js";
import { prismaKnownErrorCodes } from "~/lib/prisma.js";
import type { ResultAsync, TResult } from "~/lib/result.js";
import { Result, type ResultAsync, type TResult } from "~/lib/result.js";
import type { ProductRepository as IProductRepository } from "~/services/products/index.js";

export class ProductRepository {
export class ProductRepository implements IProductRepository {
constructor(private db: PrismaClient) {}

async findManyMerchants(
ctx: Context,
): ResultAsync<
{ merchants: MerchantType[]; total: number },
InternalServerError
> {
ctx.log.info("Finding many merchants");
try {
const findManyPromise = this.db.merchant.findMany();
const countPromise = this.db.merchant.count();
const [merchants, total] = await this.db.$transaction([
findManyPromise,
countPromise,
]);
return {
ok: true,
data: {
merchants,
total,
},
};
} catch (err) {
return Result.error(
new InternalServerError("Failed to find merchants", err),
);
}
}

/**
* createMerchant creates a merchant.
*
Expand Down
Loading

0 comments on commit 8b22b25

Please sign in to comment.