Skip to content

Commit

Permalink
feat: edit/delete dunnings (#1871)
Browse files Browse the repository at this point in the history
* feat: delete campaign

* feat: edit campaign

* fix: deserialize amount

* fix: translation keys

* fix: update router

* fix: qa return
  • Loading branch information
keellyp authored Nov 28, 2024
1 parent 9e4744c commit 1e5bb2f
Show file tree
Hide file tree
Showing 8 changed files with 2,340 additions and 1,920 deletions.
81 changes: 81 additions & 0 deletions src/components/settings/dunnings/DeleteCampaignDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { gql } from '@apollo/client'
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'

import { DialogRef } from '~/components/designSystem'
import { WarningDialog } from '~/components/WarningDialog'
import { addToast } from '~/core/apolloClient'
import { DeleteCampaignFragment, useDeleteDunningCampaignMutation } from '~/generated/graphql'
import { useInternationalization } from '~/hooks/core/useInternationalization'

gql`
fragment DeleteCampaign on DunningCampaign {
id
appliedToOrganization
}
mutation deleteDunningCampaign($input: DestroyDunningCampaignInput!) {
destroyDunningCampaign(input: $input) {
id
}
}
`

export interface DeleteCampaignDialogRef {
openDialog: (props: DeleteCampaignFragment) => unknown
closeDialog: () => unknown
}

export const DeleteCampaignDialog = forwardRef<DeleteCampaignDialogRef, unknown>((_props, ref) => {
const { translate } = useInternationalization()
const dialogRef = useRef<DialogRef>(null)
const [localData, setLocalData] = useState<DeleteCampaignFragment>()

const [deleteDunningCampaign] = useDeleteDunningCampaignMutation({
refetchQueries: ['getDunningCampaigns'],
onCompleted: ({ destroyDunningCampaign }) => {
if (!destroyDunningCampaign) {
return
}

addToast({
severity: 'success',
message: translate('text_1732187313660ayamm4mu716'),
})
},
})

useImperativeHandle(ref, () => ({
openDialog: (props) => {
setLocalData(props)
dialogRef.current?.openDialog()
},
closeDialog: () => {
setLocalData(undefined)
dialogRef.current?.closeDialog()
},
}))

return (
<WarningDialog
ref={dialogRef}
title={translate('text_17321873136600ctyqurb2n2')}
description={
localData?.appliedToOrganization
? translate('text_1732187375488dzhyehimjs3')
: translate('text_1732187375488g4igt5sf7kg')
}
onContinue={async () => {
await deleteDunningCampaign({
variables: {
input: {
id: localData?.id || '',
},
},
})
}}
continueText={translate('text_1732187313660we30lb9kg57')}
/>
)
})

DeleteCampaignDialog.displayName = 'DeleteCampaignDialog'
5 changes: 3 additions & 2 deletions src/core/router/SettingRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const XERO_INTEGRATION_ROUTE = `${INTEGRATIONS_ROUTE}/xero`
export const XERO_INTEGRATION_DETAILS_ROUTE = `${INTEGRATIONS_ROUTE}/xero/:integrationId/:tab`
export const DUNNINGS_SETTINGS_ROUTE = `${SETTINGS_ROUTE}/dunnings`
export const CREATE_DUNNING_ROUTE = `${SETTINGS_ROUTE}/dunnings/create`
export const UPDATE_DUNNING_ROUTE = `${SETTINGS_ROUTE}/dunnings/:campaignId/edit`

export const settingRoutes: CustomRouteObject[] = [
{
Expand Down Expand Up @@ -342,9 +343,9 @@ export const settingRoutes: CustomRouteObject[] = [
],
},
{
path: CREATE_DUNNING_ROUTE,
path: [CREATE_DUNNING_ROUTE, UPDATE_DUNNING_ROUTE],
private: true,
element: <CreateDunning />,
permissions: ['dunningCampaignsCreate', 'dunningCampaignsView'],
permissions: ['dunningCampaignsCreate', 'dunningCampaignsView', 'dunningCampaignsUpdate'],
},
]
3,759 changes: 1,954 additions & 1,805 deletions src/generated/graphql.tsx

Large diffs are not rendered by default.

195 changes: 195 additions & 0 deletions src/hooks/useCreateEditDunningCampaign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { gql } from '@apollo/client'
import { useEffect, useMemo } from 'react'
import { useNavigate, useParams } from 'react-router-dom'

import { addToast, hasDefinedGQLError } from '~/core/apolloClient'
import { FORM_ERRORS_ENUM } from '~/core/constants/form'
import { DUNNINGS_SETTINGS_ROUTE, ERROR_404_ROUTE } from '~/core/router'
import { serializeAmount } from '~/core/serializers/serializeAmount'
import {
CreateDunningCampaignInput,
LagoApiError,
useCreateDunningCampaignMutation,
useCreateDunningCampaignPaymentProviderQuery,
useGetSingleCampaignQuery,
useUpdateDunningCampaignMutation,
} from '~/generated/graphql'

import { DunningCampaignFormFragment } from './../generated/graphql'

export type DunningCampaignFormInput = Omit<
CreateDunningCampaignInput,
'daysBetweenAttempts' | 'maxAttempts'
> & {
daysBetweenAttempts: string
maxAttempts: string
}

gql`
fragment DunningCampaignForm on DunningCampaign {
name
code
description
thresholds {
amountCents
currency
}
daysBetweenAttempts
maxAttempts
appliedToOrganization
}
query GetSingleCampaign($id: ID!) {
dunningCampaign(id: $id) {
id
...DunningCampaignForm
}
}
query CreateDunningCampaignPaymentProvider {
paymentProviders {
collection {
__typename
}
}
}
mutation CreateDunningCampaign($input: CreateDunningCampaignInput!) {
createDunningCampaign(input: $input) {
id
...DunningCampaignForm
}
}
mutation UpdateDunningCampaign($input: UpdateDunningCampaignInput!) {
updateDunningCampaign(input: $input) {
id
...DunningCampaignForm
}
}
`

const formatPayload = (values: DunningCampaignFormInput): CreateDunningCampaignInput => {
return {
...values,
daysBetweenAttempts: Number(values.daysBetweenAttempts),
maxAttempts: Number(values.maxAttempts),
thresholds: values.thresholds.map((threshold) => ({
...threshold,
amountCents: serializeAmount(threshold.amountCents, threshold.currency),
})),
}
}

interface UseCreateEditDunningCampaignReturn {
loading: boolean
errorCode?: string
isEdition: boolean
hasPaymentProviderExcludingGoCardless: boolean
campaign?: DunningCampaignFormFragment
onSave: (value: DunningCampaignFormInput) => Promise<void>
onClose: () => void
}

export const useCreateEditDunningCampaign = (): UseCreateEditDunningCampaignReturn => {
const navigate = useNavigate()
const { campaignId } = useParams<string>()

const { data, loading, error } = useGetSingleCampaignQuery({
variables: {
id: campaignId as string,
},
skip: !campaignId,
})

const { data: paymentProviderData, loading: loadingPaymentProvider } =
useCreateDunningCampaignPaymentProviderQuery()

const [create, { error: createError }] = useCreateDunningCampaignMutation({
context: { silentErrorCodes: [LagoApiError.UnprocessableEntity] },
onCompleted({ createDunningCampaign }) {
if (!!createDunningCampaign) {
addToast({
severity: 'success',
translateKey: 'text_17290016117598ws4m1j6wvy',
})
}
navigate(DUNNINGS_SETTINGS_ROUTE)
},
})

const [update, { error: updateError }] = useUpdateDunningCampaignMutation({
context: { silentErrorCodes: [LagoApiError.UnprocessableEntity] },
onCompleted({ updateDunningCampaign }) {
if (!!updateDunningCampaign) {
addToast({
severity: 'success',
translateKey: 'text_1732187313660tetkzao72e1',
})
}
navigate(DUNNINGS_SETTINGS_ROUTE)
},
})

useEffect(() => {
if (hasDefinedGQLError('NotFound', error, 'dunningCampaign')) {
navigate(ERROR_404_ROUTE)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])

const errorCode = useMemo(() => {
if (hasDefinedGQLError('ValueAlreadyExist', createError || updateError)) {
return FORM_ERRORS_ENUM.existingCode
}

return undefined
}, [createError, updateError])

const hasPaymentProviderExcludingGoCardless =
!!paymentProviderData?.paymentProviders?.collection.filter(
(provider) => provider.__typename !== 'GocardlessProvider',
).length

return useMemo(
() => ({
loading: loading || loadingPaymentProvider,
errorCode,
isEdition: !!campaignId,
campaign: data?.dunningCampaign || undefined,
hasPaymentProviderExcludingGoCardless,
onClose: () => {
navigate(DUNNINGS_SETTINGS_ROUTE)
},
onSave: !!campaignId
? async (values) => {
await update({
variables: {
input: {
id: campaignId,
...formatPayload(values),
},
},
})
}
: async (values) => {
await create({
variables: {
input: { ...formatPayload(values) },
},
})
},
}),
[
loading,
loadingPaymentProvider,
errorCode,
campaignId,
data?.dunningCampaign,
hasPaymentProviderExcludingGoCardless,
navigate,
update,
create,
],
)
}
3 changes: 2 additions & 1 deletion src/layouts/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
settingRoutes,
SETTINGS_ROUTE,
TAXES_SETTINGS_ROUTE,
UPDATE_DUNNING_ROUTE,
UPDATE_TAX_ROUTE,
} from '~/core/router'
import { useInternationalization } from '~/hooks/core/useInternationalization'
Expand Down Expand Up @@ -45,7 +46,7 @@ const Settings = () => {
}
return acc
},
[CREATE_TAX_ROUTE, UPDATE_TAX_ROUTE, CREATE_DUNNING_ROUTE],
[CREATE_TAX_ROUTE, UPDATE_TAX_ROUTE, CREATE_DUNNING_ROUTE, UPDATE_DUNNING_ROUTE],
)

return (
Expand Down
Loading

0 comments on commit 1e5bb2f

Please sign in to comment.