Skip to content

Commit

Permalink
Allow the user to create recurring donations per campaign (#1243)
Browse files Browse the repository at this point in the history
* Recurring donations Implementation:
1. Add a checkbox for the user to choose whether it needs to be a subscrition or one time donation.
2. Add the recurring donations in the user's profile along with the option to cancel them.
3. Add the recurring donations in the admin profile.
4. Hide the anonymous donation option for subscriptions. Otherwise the user cannot cancel them.

* Add some help steps for ubuntu users during the first time install.

* Allow for later login. We might not be currently logged in, but if a recurring donation is selected, we will force the user to login on step 2.

* Use a proper mui confirmation dialog when we want to cancel a recurring donation.

* Review fixes:
1. Fix problems around editing and creating recurring donations via the admin panel.
Campaign select needs to have the current campaing assigned.
2. Don't use hard-coded paths, use routes instead.
3. Fix the typescript type issues.
4. Remove leftovers of debug information.
5. Remove vaultId, as it was duplicated from sourceVault.

* Fix typescript compilation errors. Remove a incorrect comma.

* Fix a 'prettier' problem in the frontent. Make the code more readable.

* Improve the donation playwright tests, by waiting for the clicks.
  • Loading branch information
slavcho authored Jan 8, 2023
1 parent be1580e commit 2aa7422
Show file tree
Hide file tree
Showing 41 changed files with 576 additions and 169 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@

## Initial setup

# for Ubuntu users:

# make sure cmdtest is not installed, it has a different yarn command

# for installing node.js: https://github.com/nodesource/distributions

# if you have newer node version, you can use this to downgrade:

# > sudo npm install -g n

# > sudo n stable

```shell
git clone [email protected]:podkrepi-bg/frontend.git
cd frontend
Expand Down
6 changes: 3 additions & 3 deletions e2e/local/donation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test.describe('anonymous user card donation flow', () => {
await page.locator('input[name="cardIncludeFees"]').check()
await page.locator('button:has-text("Напред")').click()

page.locator('text=Дарете анонимно').click()
await page.locator('text=Дарете анонимно').click()
await page.locator('button:has-text("Напред")').click()

await page.fill('textarea', 'е2е_tester')
Expand All @@ -39,7 +39,7 @@ test.describe('anonymous user card donation flow', () => {
await expect(page.locator('text=BGN 5.00')).toBeDefined()
await page.locator('input[name="email"]').fill('[email protected]')
await page.locator('input[name="cardNumber"]').fill('4242424242424242')
await page.locator('input[name="cardExpiry"]').fill('0424')
await page.locator('input[name="cardExpiry"]').fill('04 / 24')
await page.locator('input[name="cardCvc"]').fill('123')
await page.locator('input[name="billingName"]').fill('John Doe')
await page.locator('select[name="billingCountry"]').selectOption('BG')
Expand All @@ -62,7 +62,7 @@ test.describe('anonymous user card donation flow', () => {
await page.locator('input[name="cardIncludeFees"]').check()
await page.locator('button:has-text("Напред")').click()

page.locator('text=Дарете анонимно').click()
await page.locator('text=Дарете анонимно').click()
await page.locator('button:has-text("Напред")').click()

await page.fill('textarea', 'е2е_tester')
Expand Down
3 changes: 2 additions & 1 deletion public/locales/bg/one-time-donation.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"owner": "Сдружение Подкрепи БГ",
"bank": "Уникредит Булбанк",
"reason-donation": "Като основание за превод въведете:",
"message-warning": "Ако не въведете точно основанието, може да не успеем да разпределим парите към предназначената кампания."
"message-warning": "Ако не въведете точно основанието, може да не успеем да разпределим парите към предназначената кампания.",
"recurring-donation": "Дарявай повторно всеки месец тази сума до края на кампанията! Може да се откажете по всяко време."
},
"alerts": {
"success": "Дарението е направено успешно!",
Expand Down
21 changes: 18 additions & 3 deletions public/locales/bg/recurring-donation.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,41 @@
"edit-form-heading": "Редактирай",
"recurring-donations": "Всички повтарящи се дарения",
"recurring-donation": "Повтарящи се дарения",
"extSubscriptionId": "Абонамент",
"extSubscriptionId": "Външен абонамент",
"extCustomerId": "ID на клиент",
"campaign": "Кампания",
"currency": "Валута",
"amount": "Налични средства",
"amount": "Сума",
"status": "Статус",
"startDate": "Начална дата",
"personId": "ID на потребител",
"person": "Потребител",
"vaultId": "ID на трезор",
"vault": "Трезор",
"deleteTitle": "Сигурни ли сте?",
"deleteContent": "Това действие ще изтрие елемента завинаги!",
"actions": "Действия",
"alerts": {
"create": "Записът беше създаден успешно!",
"edit": "Записът беше редактиран успешно!",
"delete": "Записът беше изтрит успешно!",
"cancel": "Дарението беше прекратено!",
"cancel-confirm": "Сигурни ли сте, че искате да прекратите дарението?",
"error": "Възникна грешка! Моля опитайте отново по-късно."
},
"statuses": {
"active": "Активно",
"incomplete": "Непълно",
"incompleteExpired": "Непълно/изтекло",
"pastDue": "Изтекло",
"canceled": "Прекратено",
"trailing": "Изоставащо",
"unpaid": "Неплатено"
},
"cta": {
"add": "Добави",
"confirm": "Потвърди",
"cancel": "Отказ",
"cancel": "Откажи",
"delete": "Изтрий",
"edit": "Редактирай",
"details": "Детайли",
Expand Down
3 changes: 2 additions & 1 deletion public/locales/en/one-time-donation.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"owner": "Association Podkrepi BG",
"bank": "Unicredit Bulbank",
"reason-donation": "For payment reference use:",
"message-warning": "If you don't enter the exact reference we may not be able to assign the money to the desired campaign."
"message-warning": "If you don't enter the exact reference we may not be able to assign the money to the desired campaign.",
"recurring-donation": "Donate the same amount every month until the end of the campaign! Cancel anytime."
},
"alerts": {
"success": "Donation was processed successfully!",
Expand Down
45 changes: 45 additions & 0 deletions public/locales/en/recurring-donation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"form-heading": "Add",
"edit-form-heading": "Edit",
"recurring-donations": "All recurring donations",
"recurring-donation": "Recurring donations",
"extSubscriptionId": "External subscription ID",
"extCustomerId": "External customer ID",
"campaign": "Campaign",
"currency": "Currency",
"amount": "Amount",
"status": "Status",
"startDate": "Start date",
"personId": "Person ID",
"person": "Person",
"vaultId": "Vault ID",
"vault": "Vault",
"deleteTitle": "Are you sure?",
"deleteContent": "Are you sure you want to delete this recurring donation?",
"actions": "Actions",
"alerts": {
"create": "Recurring donation created successfully",
"edit": "Recurring donation edited successfully",
"delete": "Recurring donation deleted successfully",
"cancel": "Recurring donation cancelled successfully",
"error": "Error while processing your request. Please try again later."
},
"statuses": {
"active": "Active",
"incomplete": "Incomplete",
"incompleteExpired": "Expired",
"pastDue": "Past due",
"canceled": "Canceled",
"trailing": "Trailing",
"unpaid": "Unpaid"
},
"cta": {
"add": "Add",
"confirm": "Confirm",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"details": "Details",
"submit": "Submit"
}
}
2 changes: 1 addition & 1 deletion src/common/hooks/campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function useCampaignAdminList() {
export const useGetUserCampaigns = () => {
const { data: session } = useSession()
return useQuery<AdminCampaignResponse[]>(
[endpoints.campaign.getUserCamapaigns.url],
[endpoints.campaign.getUserDonatedToCampaigns.url],
authQueryFnFactory<AdminCampaignResponse[]>(session?.accessToken),
)
}
Expand Down
4 changes: 1 addition & 3 deletions src/common/hooks/donation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ export function usePriceList() {
export function useSinglePriceList() {
return useQuery<DonationPrice[]>([endpoints.donation.singlePrices.url])
}
export function useRecurringPriceList() {
return useQuery<DonationPrice[]>([endpoints.donation.recurringPrices.url])
}

export function useDonationSession() {
const { t } = useTranslation()
const mutation = useMutation<
Expand Down
20 changes: 14 additions & 6 deletions src/common/hooks/recurringDonation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ import { endpoints } from 'service/apiEndpoints'
import { authQueryFnFactory } from 'service/restRequests'
import { RecurringDonationResponse } from 'gql/recurring-donation'

export function useRecurringDonationList() {
export function useRecurringDonation(id: string) {
const { data: session } = useSession()
return useQuery<RecurringDonationResponse>(
[endpoints.recurringDonation.getRecurringDonation(id).url],
authQueryFnFactory<RecurringDonationResponse>(session?.accessToken),
)
}

export const useAllRecurringDonations = () => {
const { data: session } = useSession()
return useQuery<RecurringDonationResponse[]>(
[endpoints.recurringDonation.recurringDonation.url],
[endpoints.recurringDonation.list.url],
authQueryFnFactory<RecurringDonationResponse[]>(session?.accessToken),
)
}

export function useRecurringDonation(id: string) {
export const useGetUserRecurringDonations = () => {
const { data: session } = useSession()
return useQuery<RecurringDonationResponse>(
[endpoints.recurringDonation.getRecurringDonation(id).url],
authQueryFnFactory<RecurringDonationResponse>(session?.accessToken),
return useQuery<RecurringDonationResponse[]>(
[endpoints.recurringDonation.getUserRecurringDonations.url],
authQueryFnFactory<RecurringDonationResponse[]>(session?.accessToken),
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/common/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const routes = {
recurringDonation: {
index: '/admin/recurring-donation',
create: '/admin/recurring-donation/create',
view: (id: string) => `/admin/recurring-donation/${id}`,
edit: (id: string) => `/admin/recurring-donation/${id}`,
},
irregularity: {
index: '/admin/irregularities',
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/navigation/adminMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const menuPayments = [
{ label: 'Прехвърляния', icon: MoveUp, href: routes.admin.transfer.index },
{ label: 'Разходи', icon: Paid, href: routes.admin.expenses.index },
{
label: 'Повтарящо се дарение',
label: 'Повтарящи се дарения',
icon: VolunteerActivism,
href: routes.admin.recurringDonation.index,
},
Expand Down
26 changes: 25 additions & 1 deletion src/components/auth/profile/DonationTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import { useTranslation } from 'next-i18next'
import { moneyPublic } from 'common/util/money'
import { useUserDonations } from 'common/hooks/donation'
import { getCurrentPerson } from 'common/util/useCurrentPerson'
import { useGetUserRecurringDonations } from 'common/hooks/recurringDonation'
import { useRouter } from 'next/router'

import { ProfileTabs } from './tabs'
import ProfileTab from './ProfileTab'
import DonationTable from './DonationTable'
import { DonationStatus, PaymentProvider } from 'gql/donations.enums'
import { RecurringDonationStatus } from 'gql/recurring-donation-status.d'
import { RecurringDonationResponse } from 'gql/recurring-donation'
import MyRecurringCampaignsTable from './MyRecurringCampaignsTable'

const PREFIX = 'DonationTab'

Expand Down Expand Up @@ -75,16 +79,30 @@ const Root = styled('div')(({ theme }) => ({
},
}))

//Sum the active donations
function recurringDonationsSum(donations: RecurringDonationResponse[] | undefined) {
if (!donations) {
return 0.0
}

return donations
.filter((donation) => donation.status === RecurringDonationStatus.active)
.reduce((sum, donation) => sum + donation.amount, 0.0)
}

export default function DonationTab() {
const router = useRouter()
const { t } = useTranslation()

const { data: user } = getCurrentPerson(!!router.query?.register)

if (router.query?.register) {
delete router.query.register
router.replace({ pathname: router.pathname, query: router.query }, undefined, { shallow: true })
}
const { data: userDonations, isLoading: isUserDonationLoading } = useUserDonations()
const { data: recurringDonations } = useGetUserRecurringDonations()

return (
<Root>
<Box className={classes.boxTitle}>
Expand Down Expand Up @@ -112,7 +130,7 @@ export default function DonationTab() {
{/* <Typography>Я, Ф, М, А 2022</Typography> */}
</Box>
<Typography fontWeight="medium" variant="h6">
0,00 лв.
{moneyPublic(recurringDonationsSum(recurringDonations))}
</Typography>
</Box>
<Box className={classes.donationsBoxRow}>
Expand Down Expand Up @@ -162,6 +180,12 @@ export default function DonationTab() {
<ProfileTab name={ProfileTabs.donations}>
<DonationTable donations={userDonations?.donations} />
</ProfileTab>
<Box className={classes.boxTitle}>
<Typography className={classes.h3}>{t('profile:donations.recurringDonations')}</Typography>
</Box>
<ProfileTab name={ProfileTabs.myCampaigns}>
<MyRecurringCampaignsTable />
</ProfileTab>
</Root>
)
}
46 changes: 7 additions & 39 deletions src/components/auth/profile/DonationTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,66 +36,34 @@ function DonationTable({ donations }: DonationTableProps) {
const { t, i18n } = useTranslation()
const [fromDate, setFromDate] = React.useState<Date | null>(null)
const [toDate, setToDate] = React.useState<Date | null>(null)
const [monthly, setMonthly] = React.useState(true)
const [oneTime, setOneTime] = React.useState(true)
const filteredByTypeDonations = useMemo(() => {
if (monthly && oneTime) {
return donations
}
if (!monthly && !oneTime) {
return []
}
if (monthly) {
return donations?.filter((d) => d.type !== 'donation')
}
if (oneTime) {
return donations?.filter((d) => d.type === 'donation')
}
return donations
}, [donations, monthly, oneTime])

const filteredDonations = useMemo(() => {
if (!fromDate && !toDate) {
return filteredByTypeDonations
return donations
}
if (fromDate && toDate) {
return filteredByTypeDonations?.filter((d) => {
return donations?.filter((d) => {
const createdAtDate = parseISO(d.createdAt)
return isAfter(createdAtDate, fromDate) && isBefore(createdAtDate, toDate)
})
}
if (fromDate) {
return filteredByTypeDonations?.filter((d) => {
return donations?.filter((d) => {
const createdAtDate = parseISO(d.createdAt)
return isAfter(createdAtDate, fromDate)
})
}
if (toDate) {
return filteredByTypeDonations?.filter((d) => {
return donations?.filter((d) => {
const createdAtDate = parseISO(d.createdAt)
return isBefore(createdAtDate, toDate)
})
}
}, [filteredByTypeDonations, fromDate, toDate])
}, [donations, fromDate, toDate])

return (
<Card sx={{ padding: theme.spacing(2), boxShadow: theme.shadows[0] }}>
<Grid container alignItems={'flex-start'} spacing={theme.spacing(2)}>
<Grid item xs={6} sm={3}>
<CheckboxLabel>{t('profile:donations.oneTime')}</CheckboxLabel>
<Checkbox
onChange={(e, checked) => setOneTime(checked)}
checked={oneTime}
name="oneTime"
/>
</Grid>
{/* TODO: pending implementation on recuring donations
<Grid item xs={6} sm={3}>
<CheckboxLabel>{t('profile:donations.monthly')}</CheckboxLabel>
<Checkbox
onChange={(e, checked) => setMonthly(checked)}
checked={monthly}
name="monthly"
/>
</Grid> */}
<LocalizationProvider
adapterLocale={i18n.language === 'bg' ? bg : enUS}
dateAdapter={AdapterDateFns}>
Expand Down
1 change: 1 addition & 0 deletions src/components/auth/profile/MyDonatedToCampaignsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function MyDonatedToCampaignTable() {
width: 100,
headerAlign: 'left',
}

const columns: GridColumns = [
{
field: 'state',
Expand Down
Loading

0 comments on commit 2aa7422

Please sign in to comment.