Skip to content

Commit

Permalink
Add new paid data structures to match Bot side
Browse files Browse the repository at this point in the history
  • Loading branch information
tescher committed Sep 12, 2022
1 parent 4e748ea commit c184a57
Show file tree
Hide file tree
Showing 13 changed files with 147 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
import { useState } from 'react';
import { useContext } from 'react';
import {
claimedBy,
actionBy,
isClaimableByUser,
newActivityHistory,
newStatusHistory,
Expand All @@ -56,7 +56,7 @@ const BountyClaim = ({ bounty }: { bounty: BountyCollection }): JSX.Element => {
const confirmBounty = async () => {
if (message && user) {
const claimData: BountyClaimCollection = {
claimedBy: claimedBy(user),
claimedBy: actionBy(user),
submissionNotes: message,
status: 'In-Progress',
activityHistory: newActivityHistory(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button, Alert, AlertIcon } from '@chakra-ui/react';
import React, { useState } from 'react';
import axios from '@app/utils/AxiosUtils';
import { BountyCollection } from '@app/models/Bounty';
import { ActivityHistoryItem, BountyCollection, BountyPaidCollection } from '@app/models/Bounty';
import {
Modal,
ModalOverlay,
Expand All @@ -12,6 +12,9 @@ import {
ModalCloseButton,
} from '@chakra-ui/react';
import PAID_STATUS from '@app/constants/paidStatus';
import { actionBy, newActivityHistory } from '@app/utils/formUtils';
import ACTIVITY from '@app/constants/activity';
import { useUser } from '@app/hooks/useUser';

type SetState<T extends any> = (arg: T) => void;

Expand All @@ -29,20 +32,29 @@ const MarkPaidModal = ({
markPaidMessage: string;
}) => {
const [error, setError] = useState(false);
const { user } = useUser();

const handleMarkPaid = async () => {
await markBountiesPaid();
onClose();
};

const markBountiesPaid = async () => {
if (bounties) {
if (bounties && user) {
const markBounties = bounties?.map(async function(bounty) {
bounty.paidStatus = PAID_STATUS.PAID;
const paidData: BountyPaidCollection = {
paidBy: actionBy(user),
paidStatus: PAID_STATUS.PAID,
paidAt: new Date().toISOString(),
activityHistory: newActivityHistory(
bounty.activityHistory as ActivityHistoryItem[],
ACTIVITY.PAID
),
};
try {
const res = await axios.patch<void, any, BountyCollection>(
`api/bounties/${bounty._id}?customerId=${bounty.customerId}&force=true`,
bounty
const res = await axios.patch<void, any, BountyPaidCollection>(
`api/bounties/${bounty._id}/paid?customerId=${bounty.customerId}`,
paidData
);
if (res.status !== 200) {
setError(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const NotNeededFields = [
'reviewedBy',
'submittedBy',
'paidStatus',
'paidAt',
'paidBy',
'evergreen',
'claimLimit',
'isParent',
Expand Down
6 changes: 3 additions & 3 deletions packages/react-app/src/components/pages/Bounties/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { useUser } from '@app/hooks/useUser';
import { useRoles } from '@app/hooks/useRoles';

import SavedQueriesMenu from './Filters/SavedQueriesMenu';
import BOUNTY_STATUS from '@app/constants/bountyStatus';
import ServiceUtils from '@app/utils/ServiceUtils';

export const PAGE_SIZE = 10;

Expand Down Expand Up @@ -108,7 +108,7 @@ const SelectExport = ({
if (roles.some((r: string) => ['admin'].includes(r))) {
bountiesToMark = selectedBounties.filter((_id) => {
const bounty = bounties?.find((b) => b._id == _id);
return [BOUNTY_STATUS.IN_PROGRESS, BOUNTY_STATUS.IN_REVIEW, BOUNTY_STATUS.COMPLETED].includes(bounty?.status);
return bounty && ServiceUtils.canBePaid({ bounty });
});
setMarkPaidMessage('Mark exported claimed bounties as paid?');
} else if (
Expand All @@ -118,7 +118,7 @@ const SelectExport = ({
) {
bountiesToMark = selectedBounties.filter((_id) => {
const bounty = bounties?.find((b) => b._id == _id);
return bounty?.createdBy.discordId == user.id && [BOUNTY_STATUS.IN_PROGRESS, BOUNTY_STATUS.IN_REVIEW, BOUNTY_STATUS.COMPLETED].includes(bounty?.status);
return bounty && (bounty.createdBy.discordId == user.id) && ServiceUtils.canBePaid({ bounty });
});
setMarkPaidMessage('Mark exported claimed bounties you created as paid?');
}
Expand Down
1 change: 1 addition & 0 deletions packages/react-app/src/constants/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const ACTIVITY = {
HELP: 'help',
TAG: 'tag',
EDIT: 'edit',
PAID: 'paid',
} as const;

export const CLIENT = {
Expand Down
19 changes: 19 additions & 0 deletions packages/react-app/src/models/Bounty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ export const BountySchema = object({
season: string().optional(),

paidStatus: paidStatus.optional(),
paidAt: string().optional(),
paidBy: DiscordUser.optional(),

statusHistory: array(StatusHistory).optional(),
activityHistory: array(ActivityHistory).optional(),
Expand Down Expand Up @@ -244,8 +246,16 @@ export const BountyClaimSchema = object({
activityHistory: array(ActivityHistory).required(),
}).noUnknown(true);

export const BountyPaidSchema = object({
paidBy: DiscordUser.required(),
paidStatus: paidStatus.required(),
paidAt: string().required(),
activityHistory: array(ActivityHistory).required(),
}).noUnknown(true);

export type BountyCollection = SchemaToInterface<typeof BountySchema>;
export type BountyClaimCollection = SchemaToInterface<typeof BountyClaimSchema>;
export type BountyPaidCollection = SchemaToInterface<typeof BountyPaidSchema>;
export type StatusHistoryItem = SchemaToInterface<typeof StatusHistory>;
export type ActivityHistoryItem = SchemaToInterface<typeof ActivityHistory>;
export type DiscordBoardUser = ToInterface<typeof DiscordUser>;
Expand Down Expand Up @@ -313,6 +323,15 @@ export const BountyBoardSchema = new mongoose.Schema({
/* "Unpaid", "Paid" */
type: String,
},
paidAt: {
type: String,
},
paidBy: {
discordHandle: String,
discordId: Number,
iconUrl: String,
type: Object,
},
activityHistory: {
type: Array,
},
Expand Down
3 changes: 2 additions & 1 deletion packages/react-app/src/pages/api/bounties/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as service from '../../../services/bounty.service';
import middlewares from '../../../middlewares';
import { RoleRestrictions } from '@app/types/Role';
import MiscUtils from '../../../utils/miscUtils';
import ServiceUtils from '../../../utils/ServiceUtils';

const restrictions: RoleRestrictions = {
PATCH: ['admin', 'edit-bounties', 'edit-own-bounty'],
Expand Down Expand Up @@ -46,7 +47,7 @@ export const handler = async (
/* Edit a model by its ID */
try {
if (!forceUpdate) {
const bountyIsEditable = service.canBeEdited({ bounty });
const bountyIsEditable = ServiceUtils.canBeEdited({ bounty });
if (!bountyIsEditable) {
return res.status(400).json({
success: false,
Expand Down
3 changes: 2 additions & 1 deletion packages/react-app/src/pages/api/bounties/[id]/claim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { internalServerError, notFound } from '@app/errors';
import * as service from '@app/services/bounty.service';
import middlewares from '@app/middlewares';
import { RoleRestrictions } from '@app/types/Role';
import ServiceUtils from '@app/utils/ServiceUtils';

const restrictions: RoleRestrictions = {
PATCH: ['admin', 'claim-bounties'],
Expand Down Expand Up @@ -35,7 +36,7 @@ export const handler = async (
case 'PATCH':
/* Edit a model by its ID */
try {
const bountyIsEditable = service.canBeEdited({ bounty });
const bountyIsEditable = ServiceUtils.canBeEdited({ bounty });
if (!bountyIsEditable) {
return res.status(400).json({
success: false,
Expand Down
66 changes: 66 additions & 0 deletions packages/react-app/src/pages/api/bounties/[id]/paid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { NextApiRequest, NextApiResponse } from 'next';
import dbConnect from '@app/utils/dbConnect';
import { BountyPaidSchema } from '@app/models/Bounty';
import { internalServerError, notFound } from '@app/errors';
import * as service from '@app/services/bounty.service';
import middlewares from '@app/middlewares';
import { RoleRestrictions } from '@app/types/Role';
import ServiceUtils from '@app/utils/ServiceUtils';

const restrictions: RoleRestrictions = {
PATCH: ['admin', 'edit-bounties', 'edit-own-bounty'],
};

export const handler = async (
req: NextApiRequest,
res: NextApiResponse
): Promise<void> => {
const {
query: { id },
} = req;
if (typeof id !== 'string') {
return res.status(400).json({
success: false,
message: 'Multiple values for id are not supported',
id,
});
}
await dbConnect();

const bounty = await service.getBounty(id as string);
if (!bounty) {
return notFound(res);
}

switch (req.method) {
case 'PATCH':
/* Edit a model by its ID */
try {
const bountyIsEditable = ServiceUtils.canBePaid({ bounty });
if (!bountyIsEditable) {
return res.status(400).json({
success: false,
message: 'Unable to pay bounty, as is not in an payable status',
bountyStatus: bounty.status,
});
}
const updateBounty = await service.editBounty({
bounty,
body: req.body,
});
res.status(200).json({ success: true, data: updateBounty });
} catch (error) {
internalServerError(res);
}
break;

default:
return internalServerError(res);
}
};

export default middlewares({
schema: BountyPaidSchema,
handler,
restrictions,
});
14 changes: 0 additions & 14 deletions packages/react-app/src/services/bounty.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,20 +318,6 @@ export const getBounty = async (
return id.length === 24 ? await Bounty.findById(id) : null;
};

export const canBeEdited = ({
bounty,
}: {
bounty: BountyCollection;
}): boolean => {
/**
* We allow edits to the bounty only if the status is currently `draft` or `open`
*/
const bountyOpenForEdits = ['draft', 'open'].includes(
bounty.status.toLowerCase()
);
return bountyOpenForEdits;
};

type EditBountyProps = {
bounty: BountyCollection;
body: Record<string, unknown>;
Expand Down
27 changes: 27 additions & 0 deletions packages/react-app/src/utils/ServiceUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import BOUNTY_STATUS from '@app/constants/bountyStatus';
import { BountyCollection } from '@app/models/Bounty';

export default {
formatDisplayDate(dateIso: string): string {
const options: Intl.DateTimeFormatOptions = {
Expand All @@ -12,4 +15,28 @@ export default {
const uri = process.env.MONGODB_URI;
return uri || '';
},
canBeEdited({
bounty,
}: {
bounty: BountyCollection;
}): boolean {
/**
* We allow edits to the bounty only if the status is currently `draft` or `open`
*/
const bountyOpenForEdits = [BOUNTY_STATUS.DRAFT, BOUNTY_STATUS.OPEN].includes(
bounty.status
);
return bountyOpenForEdits;
},
canBePaid({
bounty,
}: {
bounty: BountyCollection;
}): boolean {
const bountyOpenForPaying = [BOUNTY_STATUS.IN_PROGRESS, BOUNTY_STATUS.IN_REVIEW, BOUNTY_STATUS.COMPLETED].includes(
bounty.status
);
return bountyOpenForPaying;
},

};
2 changes: 1 addition & 1 deletion packages/react-app/src/utils/formUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const validNonNegativeDecimal = (v: string): string | boolean => {
return Number(v) > 0 ? true : 'Must be > 0';
};

export const claimedBy = (user: APIUser): DiscordBoardUser => ({
export const actionBy = (user: APIUser): DiscordBoardUser => ({
discordHandle: `${user?.username}#${user.discriminator}`,
discordId: user?.id,
iconUrl: '',
Expand Down
7 changes: 4 additions & 3 deletions packages/react-app/tests/unit/pages/api/bounties/id.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { handler } from '../../../../../src/pages/api/bounties/[id]';
import * as service from '../../../../../src/services/bounty.service';
import { testBounty } from '../../../../stubs/bounty.stub';
import validate from '../../../../../src/middlewares/validate';
import ServiceUtils from '../../../../../src/utils/ServiceUtils';

// Prevent code from firing on import by mocking the whole module
jest.mock('../../../../../src/utils/dbConnect', () => ({
Expand Down Expand Up @@ -46,7 +47,7 @@ describe('Testing the bounty API handler', () => {
});

it('Throws an error if the patch request cannot be edited', async () => {
jest.spyOn(service, 'canBeEdited')
jest.spyOn(ServiceUtils, 'canBeEdited')
.mockReturnValue(false);

req.method = 'PATCH';
Expand All @@ -56,7 +57,7 @@ describe('Testing the bounty API handler', () => {
});

it('Edits if the patch request can be edited', async () => {
jest.spyOn(service, 'canBeEdited')
jest.spyOn(ServiceUtils, 'canBeEdited')
.mockReturnValue(true);
jest.spyOn(service, 'editBounty')
.mockReturnValue(Promise.resolve({} as BountyCollection));
Expand All @@ -67,7 +68,7 @@ describe('Testing the bounty API handler', () => {
});

it('Edits if the patch request includes a FORCE option', async () => {
jest.spyOn(service, 'canBeEdited')
jest.spyOn(ServiceUtils, 'canBeEdited')
.mockReturnValue(false);
jest.spyOn(service, 'editBounty')
.mockReturnValue(Promise.resolve({} as BountyCollection));
Expand Down

0 comments on commit c184a57

Please sign in to comment.