Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cancelUserTicketAddons mutation #303

Merged
merged 2 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/generated/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,16 @@ type Mutation {
"""
cancelUserTicket(userTicketId: String!): UserTicket!

"""
Cancel addons for multiple user tickets
"""
cancelUserTicketAddons(
"""
The IDs of the user ticket addons to cancel
"""
userTicketAddonIds: [String!]!
): [UserTicketAddon!]!

"""
Check the status of a purchase order
"""
Expand Down
6 changes: 6 additions & 0 deletions src/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,8 @@ export type Mutation = {
approvalUserTicket: UserTicket;
/** Cancel a ticket */
cancelUserTicket: UserTicket;
/** Cancel addons for multiple user tickets */
cancelUserTicketAddons: Array<UserTicketAddon>;
/** Check the status of a purchase order */
checkPurchaseOrderStatus: PurchaseOrder;
/** Attempt to claim and/or transfer tickets */
Expand Down Expand Up @@ -500,6 +502,10 @@ export type MutationCancelUserTicketArgs = {
userTicketId: Scalars["String"]["input"];
};

export type MutationCancelUserTicketAddonsArgs = {
userTicketAddonIds: Array<Scalars["String"]["input"]>;
};

export type MutationCheckPurchaseOrderStatusArgs = {
input: CheckForPurchaseOrderInput;
};
Expand Down
184 changes: 183 additions & 1 deletion src/schema/userTicketsAddons/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { eq } from "drizzle-orm";
import { eq, inArray } from "drizzle-orm";
import { GraphQLError } from "graphql";

import { builder } from "~/builder";
import {
userTicketAddonsSchema,
selectPurchaseOrdersSchema,
purchaseOrdersSchema,
UserTicketAddonApprovalStatus,
AddonConstraintType,
} from "~/datasources/db/schema";
import { applicationError, ServiceErrors } from "~/errors";
import { handlePaymentLinkGeneration } from "~/schema/purchaseOrder/actions";
Expand All @@ -20,6 +22,7 @@ import {
claimUserTicketAddonsHelpers,
} from "./helpers";
import { PurchaseOrderRef } from "../purchaseOrder/types";
import { UserTicketAddonRef } from "../ticketAddons/types";

const ClaimUserTicketAddonInput = builder
.inputRef<{
Expand Down Expand Up @@ -256,3 +259,182 @@ function handleClaimError(error: unknown): RedeemUserTicketAddonsErrorType {
errorMessage: "An unknown error occurred",
};
}

builder.mutationField("cancelUserTicketAddons", (t) =>
t.field({
description: "Cancel multiple user ticket addons",
type: [UserTicketAddonRef],
args: {
userTicketAddonIds: t.arg({
type: ["String"],
required: true,
description: "The IDs of the user ticket addons to cancel",
}),
},
authz: {
rules: ["IsAuthenticated"],
},
resolve: async (root, { userTicketAddonIds }, context) => {
const { USER, DB, logger } = context;

if (!USER) {
throw new GraphQLError("User not found");
}

if (userTicketAddonIds.length === 0) {
throw applicationError(
"No user ticket addons provided",
ServiceErrors.INVALID_ARGUMENT,
logger,
);
}

return await DB.transaction(async (trx) => {
try {
const allUserTickets = await trx.query.userTicketsSchema.findMany({
where: (etc, ops) => {
return ops.eq(etc.userId, USER.id);
},
columns: {
id: true,
},
with: {
userTicketAddons: {
columns: {
id: true,
addonId: true,
approvalStatus: true,
},
with: {
purchaseOrder: {
columns: {
id: true,
paymentPlatform: true,
},
},
addon: {
columns: {
id: true,
},
with: {
constraints: {
columns: {
id: true,
constraintType: true,
relatedAddonId: true,
},
},
},
},
},
},
},
});

// Fetch the user ticket addons and validate ownership
const userTicketAddonsToCancel = allUserTickets
.flatMap((ut) => ut.userTicketAddons)
.filter((uta) => userTicketAddonIds.includes(uta.id));

// Validate that all requested addons exist and belong to the user
const notFoundUserTicketAddonIds = userTicketAddonIds.filter(
(id) => !userTicketAddonsToCancel.some((uta) => uta.id === id),
);

if (notFoundUserTicketAddonIds.length > 0) {
throw applicationError(
`Some user ticket addons were not found or don't belong to the user: ${notFoundUserTicketAddonIds.join(
", ",
)}`,
ServiceErrors.NOT_FOUND,
logger,
);
}

// Check for already cancelled addons
const alreadyCancelledAddons = userTicketAddonsToCancel.filter(
(uta) =>
uta.approvalStatus === UserTicketAddonApprovalStatus.CANCELLED,
);

if (alreadyCancelledAddons.length > 0) {
throw applicationError(
`Some addons are already cancelled: ${alreadyCancelledAddons
.map((ada) => ada.id)
.join(", ")}`,
ServiceErrors.FAILED_PRECONDITION,
logger,
);
}

// Fetch all user ticket addons to check dependencies
// We only consider approved addons to check dependencies
const otherUserTicketAddons = allUserTickets
.flatMap((ut) => ut.userTicketAddons)
.filter(
(uta) =>
!userTicketAddonIds.includes(uta.id) &&
uta.approvalStatus === UserTicketAddonApprovalStatus.APPROVED,
);

// TODO: Future improvements needed:
// 1. Handle paid addons cancellation - requires refund logic implementation
// 2. Consider implementing a cancellation window/policy
// 3. Consider support for partial refunds based on time until event
// 4. Consider option to cascade cancel dependent addons if the user confirms it

// For now, we only allow addons with no dependencies
for (const addonToCancel of userTicketAddonsToCancel) {
const dependentAddons = otherUserTicketAddons.filter((uta) => {
// Check if any of the user's active addons depend on the addon being cancelled
return uta.addon.constraints.some(
(constraint) =>
constraint.constraintType ===
AddonConstraintType.DEPENDENCY &&
constraint.relatedAddonId === addonToCancel.addonId,
);
});

if (dependentAddons.length > 0) {
throw applicationError(
`NOT SUPPORTED: Cannot cancel addon ${
addonToCancel.addonId
} because other addons depend on it: ${dependentAddons
.map((da) => da.addonId)
.join(", ")}`,
ServiceErrors.CONFLICT,
logger,
);
}
}

// For now, we only allow canceling free addons
for (const addon of userTicketAddonsToCancel) {
if (addon.purchaseOrder.paymentPlatform !== null) {
throw applicationError(
"NOT SUPPORTED: Cancellation of paid addons is not supported yet",
ServiceErrors.FAILED_PRECONDITION,
logger,
);
}
}

// Cancel the addons by updating their status
const cancelledAddons = await trx
.update(userTicketAddonsSchema)
.set({
approvalStatus: UserTicketAddonApprovalStatus.CANCELLED,
updatedAt: new Date(),
})
.where(inArray(userTicketAddonsSchema.id, userTicketAddonIds))
.returning();

return cancelledAddons;
} catch (e: unknown) {
logger.error("Error cancelling user ticket addons", e);
throw e;
}
});
},
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable */
/* @ts-nocheck */
/* prettier-ignore */
/* This file is automatically generated using `npm run graphql:types` */
import type * as Types from '../../../generated/types';

import type { JsonObject } from "type-fest";
import gql from 'graphql-tag';
export type CancelUserTicketAddonsMutationVariables = Types.Exact<{
userTicketAddonIds: Array<Types.Scalars['String']['input']> | Types.Scalars['String']['input'];
}>;


export type CancelUserTicketAddonsMutation = { __typename?: 'Mutation', cancelUserTicketAddons: Array<{ __typename?: 'UserTicketAddon', id: string, approvalStatus: Types.UserTicketAddonApprovalStatus, quantity: number, addonId: string, userTicketId: string }> };


export const CancelUserTicketAddons = gql`
mutation cancelUserTicketAddons($userTicketAddonIds: [String!]!) {
cancelUserTicketAddons(userTicketAddonIds: $userTicketAddonIds) {
id
approvalStatus
quantity
addonId
userTicketId
}
}
`;
9 changes: 9 additions & 0 deletions src/schema/userTicketsAddons/tests/cancelUserTicketAddons.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mutation CancelUserTicketAddons($userTicketAddonIds: [String!]!) {
cancelUserTicketAddons(userTicketAddonIds: $userTicketAddonIds) {
id
approvalStatus
quantity
addonId
userTicketId
}
}
Loading
Loading