From 5b5edd2967e43e6fb3971bc646546e8abf5f0e7a Mon Sep 17 00:00:00 2001 From: Nnachevvv Date: Sat, 28 Oct 2023 23:01:12 +0300 Subject: [PATCH] Add refund button --- public/locales/bg/donations.json | 10 +++ public/locales/bg/profile.json | 1 + .../admin/donations/DonationsPage.tsx | 2 + src/components/admin/donations/grid/Grid.tsx | 38 +++++++- .../admin/donations/modals/RefundModal.tsx | 89 +++++++++++++++++++ src/gql/donations.d.ts | 22 +++++ src/service/apiEndpoints.ts | 2 + src/service/donation.ts | 12 +++ 8 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/components/admin/donations/modals/RefundModal.tsx diff --git a/public/locales/bg/donations.json b/public/locales/bg/donations.json index 67c043af1..62daeb007 100644 --- a/public/locales/bg/donations.json +++ b/public/locales/bg/donations.json @@ -34,6 +34,7 @@ "edit": "Дарението беше редактирано успешно!", "delete": "Дарението беше изтрито успешно!", "deleteAll": "Даренията бяха изтрити успешно!", + "refundSuccess": "Беше създадена успешна заявка за връщане на парите към Stripe", "editDonor": "Дарителят беше редактиран успешно!", "error": "Възникна грешка! Моля опитайте отново по-късно.", "requiredError": "Полето е задължително." @@ -60,5 +61,14 @@ "sortBy": "Сортиране", "minAmount": "Мин сума", "maxAmount": "Макс сума" + }, + "refund": { + "icon": "Връщане на дарение", + "title": "Направи заявка за връщане на дарение", + "confirmation": "Сигурни ли сте, че искате да възстановите сумата от дарение:", + "number": "Номер:", + "amount": "Сума:", + "email": "Е-mail на дарител:", + "confirm-button": "Възстановяване парите от даренеие" } } diff --git a/public/locales/bg/profile.json b/public/locales/bg/profile.json index a03fc02a7..9ba5ca6cd 100644 --- a/public/locales/bg/profile.json +++ b/public/locales/bg/profile.json @@ -43,6 +43,7 @@ "initial": "започнато", "waiting": "чакащо", "succeeded": "успешно", + "refund": "възстановено", "cancelled": "отменено" } }, diff --git a/src/components/admin/donations/DonationsPage.tsx b/src/components/admin/donations/DonationsPage.tsx index 8172dec8f..88e1a041f 100644 --- a/src/components/admin/donations/DonationsPage.tsx +++ b/src/components/admin/donations/DonationsPage.tsx @@ -5,8 +5,10 @@ import AdminLayout from 'components/common/navigation/AdminLayout' import Grid from './grid/Grid' import GridAppbar from './grid/GridAppbar' import GridFilters from './grid/GridFilters' +import { RefundStoreImpl } from './store/RefundStore' export const ModalStore = new ModalStoreImpl() +export const RefundStore = new RefundStoreImpl() export default function DocumentsPage() { const { t } = useTranslation() diff --git a/src/components/admin/donations/grid/Grid.tsx b/src/components/admin/donations/grid/Grid.tsx index 12d84eaf9..1e429af73 100644 --- a/src/components/admin/donations/grid/Grid.tsx +++ b/src/components/admin/donations/grid/Grid.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { UseQueryResult } from '@tanstack/react-query' import { useTranslation } from 'next-i18next' -import { Box, Tooltip } from '@mui/material' +import { Box, IconButton, Tooltip } from '@mui/material' import { Edit } from '@mui/icons-material' import { DataGrid, @@ -15,7 +15,7 @@ import { useDonationsList } from 'common/hooks/donation' import DetailsModal from '../modals/DetailsModal' import DeleteModal from '../modals/DeleteModal' -import { ModalStore } from '../DonationsPage' +import { ModalStore, RefundStore } from '../DonationsPage' import { getExactDateTime } from 'common/util/date' import { useRouter } from 'next/router' import { money } from 'common/util/money' @@ -25,6 +25,8 @@ import theme from 'common/theme' import RenderEditPersonCell from './RenderEditPersonCell' import { useStores } from '../../../../common/hooks/useStores' import RenderEditBillingEmailCell from './RenderEditBillingEmailCell' +import RestoreIcon from '@mui/icons-material/Restore' +import RefundModal from '../modals/RefundModal' interface RenderCellProps { params: GridRenderCellParams @@ -47,6 +49,7 @@ export default observer(function Grid() { const { t } = useTranslation() const router = useRouter() const { isDetailsOpen } = ModalStore + const { isRefundOpen } = RefundStore const campaignId = router.query.campaignId as string | undefined const { @@ -136,7 +139,37 @@ export default observer(function Grid() { headerAlign: 'left', } + const { showRefund, setSelectedRecord } = RefundStore + + function refundClickClickHandler(id: string) { + setSelectedRecord({ id }) + showRefund() + } + const columns: GridColDef[] = [ + { + field: 'actions', + headerName: 'Actions', + type: 'actions', + width: 120, + resizable: false, + renderCell: (params: GridRenderCellParams) => { + return params.row?.status === 'succeeded' ? ( + <> + + refundClickClickHandler(params.row.id)}> + + + + + ) : ( + '' + ) + }, + }, { field: 'createdAt', headerName: t('donations:date'), @@ -256,6 +289,7 @@ export default observer(function Grid() { {/* making sure we don't sent requests to the API when not needed */} {isDetailsOpen && } + {isRefundOpen && } ) diff --git a/src/components/admin/donations/modals/RefundModal.tsx b/src/components/admin/donations/modals/RefundModal.tsx new file mode 100644 index 000000000..101e20352 --- /dev/null +++ b/src/components/admin/donations/modals/RefundModal.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react' +import { useTranslation } from 'next-i18next' + +import { Dialog, Typography, DialogTitle, DialogContent, Grid, CardContent } from '@mui/material' +import { useRefundStripeDonation } from 'service/donation' +import { AlertStore } from 'stores/AlertStore' +import { UseQueryResult, useMutation } from '@tanstack/react-query' +import SubmitButton from 'components/common/form/SubmitButton' +import { DonationResponse, StripeRefundRequest } from 'gql/donations' +import CloseModalButton from 'components/common/CloseModalButton' +import { useDonation } from 'common/hooks/donation' +import { observer } from 'mobx-react' +import { RefundStore } from '../DonationsPage' +import GenericForm from 'components/common/form/GenericForm' +import { fromMoney } from 'common/util/money' + +export default observer(function RefundModal() { + const { t } = useTranslation('donations') + const { isRefundOpen, hideRefund, selectedRecord } = RefundStore + const { data }: UseQueryResult = useDonation(selectedRecord.id) + + const initialValues: StripeRefundRequest = { + extPaymentIntentId: '', + } + + if (data) { + initialValues.extPaymentIntentId = data.extPaymentIntentId + } + + const refundMutation = useMutation({ + mutationFn: useRefundStripeDonation(), + onError: () => AlertStore.show(t('alerts.error'), 'error'), + onSuccess: () => { + AlertStore.show(t('alerts.refundSuccess'), 'success') + hideRefund() + }, + }) + const [loading, setLoading] = useState(false) + + async function onSubmit(values: StripeRefundRequest) { + setLoading(true) + try { + await refundMutation.mutateAsync(values.extPaymentIntentId) + } finally { + setLoading(false) + } + } + + return ( + + + + + + + {t('refund.title')} + + + + + {t('refund.confirmation')} + + {t('refund.number')} {data?.id} + + + {t('refund.amount')} {fromMoney(data?.amount as number)} {data?.currency} + + {t('refund.email')} {data?.billingEmail} + + + + + + + + + + ) +}) diff --git a/src/gql/donations.d.ts b/src/gql/donations.d.ts index 9f4c5cfe9..d6ae76772 100644 --- a/src/gql/donations.d.ts +++ b/src/gql/donations.d.ts @@ -39,6 +39,7 @@ export type DonationResponse = { updatedAt: DateTime currency: Currency amount: number + billingEmail?: string personId?: UUID person?: { id: string @@ -171,6 +172,27 @@ export type SecondStep = { export type ThirdStep = { message?: string } + +export type StripeRefundRespone = { + id: UUID + object: string + amount: number + balance_transaction: string + charge: string + created: number + currency: string + payment_intent: string + reason: string + receipt_number: string + source_transfer_reversal: string + status: DonationStatus + transfer_reversal: string +} + +export type StripeRefundRequest = { + extPaymentIntentId: string +} + export type BankTransactionsFileFormData = { bankTransactionsFileId: string } diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index 1c907ba48..c5de99b86 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -93,6 +93,8 @@ export const endpoints = { createCheckoutSession: { url: '/donation/create-checkout-session', method: 'POST' }, createPaymentIntent: { url: '/donation/create-payment-intent', method: 'POST' }, createBankDonation: { url: '/donation/create-bank-payment', method: 'POST' }, + refundStripePayment: (id: string) => + { url: `/donation/refund-stripe-payment/${id}`, method: 'POST' }, getDonation: (id: string) => { url: `/donation/${id}`, method: 'GET' }, donationsList: ( campaignId?: string, diff --git a/src/service/donation.ts b/src/service/donation.ts index 71ad192ae..e181d935e 100644 --- a/src/service/donation.ts +++ b/src/service/donation.ts @@ -8,6 +8,7 @@ import { CheckoutSessionResponse, DonationBankInput, DonationResponse, + StripeRefundRespone, UserDonationInput, } from 'gql/donations' import { apiClient } from 'service/apiClient' @@ -67,6 +68,17 @@ export function useDeleteDonation(ids: string[]) { } } +export const useRefundStripeDonation = () => { + const { data: session } = useSession() + return async (extPaymentId: string) => { + return await apiClient.post>( + endpoints.donation.refundStripePayment(extPaymentId).url, + '', + authConfig(session?.accessToken), + ) + } +} + export const useUploadBankTransactionsFiles = () => { const { data: session } = useSession() return async ({