Skip to content

Commit

Permalink
feat: Allow to dispute invoices
Browse files Browse the repository at this point in the history
  • Loading branch information
sarkissianraffi authored and ansmonjol committed Apr 11, 2024
1 parent bbedd5f commit 7486e28
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 35 deletions.
10 changes: 10 additions & 0 deletions ditto/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,16 @@
"text_649c54823c9089006247625a": "Can’t be prorated due to {{chargeModel}} charge model defined above.",
"text_649c49bcebd91c0082d84446": "Full charge",
"text_649c54823c90890062476259": "Based on the event timestamp and end date of the billing period.",
"text_66141e30699a0631f0b2ec7f": "There is no invoices with dispute lost",
"text_66141e30699a0631f0b2ec87": "Disputed lost invoices occur when a customer chooses not to pay, requests a dispute, and demands a chargeback.",
"text_66141e30699a0631f0b2ec59": "Mark this invoice as disputed",
"text_66141e30699a0631f0b2ec61": "If for any reason your customer does not want to pay this invoice, mark it as disputed. Please note that this action is irreversible and will not trigger any specific flow in the connected payment provider. Are you sure to proceed?",
"text_66141e30699a0631f0b2ec71": "Dispute invoice",
"text_66178d027e220e00dff9f67d": "This invoice cannot be voided because it has been disputed",
"text_66141e30699a0631f0b2ec9c": "Dispute lost",
"text_66141e30699a0631f0b2ed2c": "Dispute lost on {{date}}",
"text_66141e30699a0631f0b2ed32": "Disputed",
"text_66141e9feef09978ae251222": "Invoice disputed successfully",
"text_657078c28394d6b1ae1b974d": "Connect to Lago EU tax management",
"text_657078c28394d6b1ae1b9759": "This integration automates customer tax calculations based on organization country. For more information, please refer to <a rel=\"noopener noreferrer external\" target=\"_blank\" href=\"{{href}}\">Lago’s documentation</a>",
"text_657078c28394d6b1ae1b9765": "Organization country",
Expand Down
2 changes: 2 additions & 0 deletions ditto/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -346,5 +346,7 @@ sources:
fileName: 👍 [Ready for dev] - Plans - Invoice minimum spending
- name: 👍 [Ready for dev] - Onboarding - Create/Connect Lago orga via SSO
id: 660bf95b851f012f6f11ecd0
- name: ⚙️ [WIP] - Invoices - Dispute payment intent
id: 66141e2ffa16c75cb553dbc1
format: flat
variants: true
5 changes: 4 additions & 1 deletion src/components/creditNote/CreditNoteFormCalculation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ gql`
feesAmountCents
currency
versionNumber
paymentDisputeLostAt
fees {
id
appliedTaxes {
Expand Down Expand Up @@ -94,7 +95,9 @@ export const CreditNoteFormCalculation = ({
setPayBackValidation,
}: CreditNoteFormCalculationProps) => {
const { translate } = useInternationalization()
const canOnlyCredit = invoice?.paymentStatus !== InvoicePaymentStatusTypeEnum.Succeeded
const canOnlyCredit =
invoice?.paymentStatus !== InvoicePaymentStatusTypeEnum.Succeeded ||
!!invoice.paymentDisputeLostAt
const currency = invoice?.currency || CurrencyEnum.Usd
const currencyPrecision = getCurrencyPrecision(currency)
const isLegacyInvoice = (invoice?.versionNumber || 0) < 3
Expand Down
16 changes: 16 additions & 0 deletions src/components/designSystem/Status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum StatusEnum {
draft = 'draft',
failed = 'failed',
error = 'error',
disputeLost = 'disputeLost',
voided = 'voided',
}

Expand Down Expand Up @@ -51,6 +52,10 @@ const STATUS_CONFIG: {
label: 'text_624efab67eb2570101d11826',
color: theme.palette.error[600],
},
[StatusEnum.disputeLost]: {
label: 'text_66141e30699a0631f0b2ec9c',
color: theme.palette.error[600],
},
[StatusEnum.voided]: {
label: 'text_6376641a2a9c70fff5bddcd5',
color: 'input',
Expand Down Expand Up @@ -86,6 +91,17 @@ export const Status = ({ type, className, label, hideLabel = false }: StatusProp
)}
</Container>
)
case StatusEnum.disputeLost:
return (
<Container data-test={type} className={className}>
<svg height={STATUS_SIZE} width={STATUS_SIZE}>
<circle cx="6" cy="6" r="6" fill={config.color} />
</svg>
{!hideLabel && (
<Typography color="textSecondary">{label ?? translate(config.label)}</Typography>
)}
</Container>
)
default:
return (
<Container data-test={type} className={className}>
Expand Down
75 changes: 75 additions & 0 deletions src/components/invoices/DisputeInvoiceDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 {
AllInvoiceDetailsForCustomerInvoiceDetailsFragmentDoc,
useDisputeInvoiceMutation,
} from '~/generated/graphql'
import { useInternationalization } from '~/hooks/core/useInternationalization'

gql`
mutation disputeInvoice($input: LoseInvoiceDisputeInput!) {
loseInvoiceDispute(input: $input) {
id
status
...AllInvoiceDetailsForCustomerInvoiceDetails
}
}
# Fragments needed to refresh data from other parts of the UI
${AllInvoiceDetailsForCustomerInvoiceDetailsFragmentDoc}
`

type DisputeInvoiceDialogProps = {
id: string
}

export interface DisputeInvoiceDialogRef {
openDialog: (dialogData: DisputeInvoiceDialogProps) => unknown
closeDialog: () => unknown
}

export const DisputeInvoiceDialog = forwardRef<DisputeInvoiceDialogRef>((_, ref) => {
const dialogRef = useRef<DialogRef>(null)
const { translate } = useInternationalization()
const [dialogData, setDialogData] = useState<DisputeInvoiceDialogProps | undefined>(undefined)

const [disputeInvoice] = useDisputeInvoiceMutation({
onCompleted(data) {
if (data && data.loseInvoiceDispute) {
addToast({
message: translate('text_66141e9feef09978ae251222'),
severity: 'success',
})
}
},
refetchQueries: ['getInvoiceDetails'],
})

useImperativeHandle(ref, () => ({
openDialog: (data) => {
setDialogData(data)
dialogRef.current?.openDialog()
},
closeDialog: () => dialogRef.current?.closeDialog(),
}))

return (
<WarningDialog
ref={dialogRef}
title={translate('text_66141e30699a0631f0b2ec59')}
description={translate('text_66141e30699a0631f0b2ec61')}
onContinue={async () =>
await disputeInvoice({
variables: { input: { id: dialogData?.id as string } },
})
}
continueText={translate('text_66141e30699a0631f0b2ec71')}
/>
)
})

DisputeInvoiceDialog.displayName = 'DisputeInvoiceDialog'
2 changes: 1 addition & 1 deletion src/components/invoices/FinalizeInvoiceDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const FinalizeInvoiceDialog = forwardRef<FinalizeInvoiceDialogRef>((_, re
const [invoice, setInvoice] = useState<InvoiceForFinalizeInvoiceFragment>()
const [finalizeInvoice] = useFinalizeInvoiceMutation({
variables: { input: { id: invoice?.id || '' } },
refetchQueries: ['getCustomerInvoices'],
refetchQueries: ['getCustomerInvoices', 'getInvoiceDetails'],
onCompleted({ finalizeInvoice: finalizeInvoiceRes }) {
if (finalizeInvoiceRes?.id) {
addToast({
Expand Down
17 changes: 17 additions & 0 deletions src/components/invoices/InvoiceCustomerInfos.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { gql } from '@apollo/client'
import { DateTime } from 'luxon'
import { memo } from 'react'
import { generatePath, Link } from 'react-router-dom'
import styled from 'styled-components'
Expand All @@ -24,6 +25,7 @@ gql`
paymentDueDate
status
paymentStatus
paymentDisputeLostAt
customer {
id
name
Expand Down Expand Up @@ -255,6 +257,21 @@ export const InvoiceCustomerInfos = memo(({ invoice }: InvoiceCustomerInfosProps
)}
</Typography>
</InfoLine>
{!!invoice?.paymentDisputeLostAt && (
<InfoLine>
<Typography variant="caption" color="grey600" noWrap>
{translate('text_66141e30699a0631f0b2ed32')}
</Typography>
<Typography variant="body" color="grey700">
<Status
type="disputeLost"
label={translate('text_66141e30699a0631f0b2ed2c', {
date: DateTime.fromISO(invoice?.paymentDisputeLostAt).toFormat('LLL. dd, yyyy'),
})}
/>
</Typography>
</InfoLine>
)}
</div>
</Wrapper>
)
Expand Down
14 changes: 10 additions & 4 deletions src/components/invoices/InvoiceListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ gql`
totalAmountCents
currency
voidable
paymentDisputeLostAt
customer {
id
name
Expand Down Expand Up @@ -186,10 +187,14 @@ export const InvoiceListItem = ({
<Container {...props}>
<Item className={className} to={to} tabIndex={0} {...navigationProps} $context={context}>
<GridItem $context={context}>
<Status
type={statusConfig?.type as StatusEnum}
label={translate(statusConfig?.label || '')}
/>
{!!invoice.paymentDisputeLostAt ? (
<Status type="disputeLost" label={translate('text_66141e30699a0631f0b2ed32')} />
) : (
<Status
type={statusConfig?.type as StatusEnum}
label={translate(statusConfig?.label || '')}
/>
)}
<Typography variant="captionCode" color="grey700" noWrap>
{number}
</Typography>
Expand Down Expand Up @@ -251,6 +256,7 @@ export const InvoiceListItem = ({
<Button
startIcon="push"
variant="quaternary"
disabled={!!invoice.paymentDisputeLostAt}
align="left"
onClick={async () => {
const { errors } = await retryCollect({
Expand Down
148 changes: 123 additions & 25 deletions src/generated/graphql.tsx

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/hooks/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const fourOFourInvoiceMock = () => ({
id: INVOICE_FIXTURE_ID,
refundableAmountCents: '0',
creditableAmountCents: '0',
paymentDisputeLostAt: null,
invoiceType: InvoiceTypeEnum.Subscription,
fees: [],
invoiceSubscriptions: [],
Expand All @@ -32,6 +33,7 @@ export const fullSubscriptionInvoiceMockAndExpect = () => ({
refundableAmountCents: '0',
creditableAmountCents: '2700000',
invoiceType: InvoiceTypeEnum.Subscription,
paymentDisputeLostAt: null,
fees: [
{
id: 'fee-1-id',
Expand Down Expand Up @@ -134,6 +136,7 @@ export const fullSubscriptionInvoiceGroupTrueUpMockAndExpect = () => ({
refundableAmountCents: '0',
creditableAmountCents: '62833',
invoiceType: InvoiceTypeEnum.Subscription,
paymentDisputeLostAt: null,
fees: [
{
id: 'b7e53061-73b5-46bf-8f1a-2f49d50c672c',
Expand Down Expand Up @@ -607,6 +610,7 @@ export const fullOneOffInvoiceMockAndExpect = () => ({
refundableAmountCents: '0',
creditableAmountCents: '2700000',
invoiceType: InvoiceTypeEnum.OneOff,
paymentDisputeLostAt: null,
fees: [
{
id: 'fee-1-id',
Expand Down
28 changes: 27 additions & 1 deletion src/layouts/CustomerInvoiceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
} from '~/components/designSystem'
import { GenericPlaceholder } from '~/components/GenericPlaceholder'
import { AddMetadataDrawer, AddMetadataDrawerRef } from '~/components/invoices/AddMetadataDrawer'
import {
DisputeInvoiceDialog,
DisputeInvoiceDialogRef,
} from '~/components/invoices/DisputeInvoiceDialog'
import {
UpdateInvoicePaymentStatusDialog,
UpdateInvoicePaymentStatusDialogRef,
Expand Down Expand Up @@ -82,6 +86,7 @@ gql`
refundableAmountCents
creditableAmountCents
voidable
paymentDisputeLostAt
customer {
...CustomerMetadatasForInvoiceOverview
}
Expand Down Expand Up @@ -163,6 +168,7 @@ const CustomerInvoiceDetails = () => {
const updateInvoicePaymentStatusDialog = useRef<UpdateInvoicePaymentStatusDialogRef>(null)
const addMetadataDrawerDialogRef = useRef<AddMetadataDrawerRef>(null)
const voidInvoiceDialogRef = useRef<VoidInvoiceDialogRef>(null)
const disputeInvoiceDialogRef = useRef<DisputeInvoiceDialogRef>(null)
const [refreshInvoice, { loading: loadingRefreshInvoice }] = useRefreshInvoiceMutation({
variables: { input: { id: invoiceId || '' } },
})
Expand Down Expand Up @@ -435,13 +441,30 @@ const CustomerInvoiceDetails = () => {
</Button>
</>
)}
{status === InvoiceStatusTypeEnum.Finalized &&
!data?.invoice?.paymentDisputeLostAt && (
<Button
variant="quaternary"
align="left"
onClick={() => {
disputeInvoiceDialogRef.current?.openDialog({ id: data?.invoice?.id || '' })
closePopper()
}}
>
{translate('text_66141e30699a0631f0b2ec71')}
</Button>
)}
{status === InvoiceStatusTypeEnum.Finalized &&
[
InvoicePaymentStatusTypeEnum.Pending,
InvoicePaymentStatusTypeEnum.Failed,
].includes(paymentStatus) && (
<Tooltip
title={translate('text_65269c2e471133226211fdd0')}
title={translate(
!!data?.invoice?.paymentDisputeLostAt
? 'text_66178d027e220e00dff9f67d'
: 'text_65269c2e471133226211fdd0',
)}
placement="bottom-end"
disableHoverListener={voidable}
>
Expand Down Expand Up @@ -494,6 +517,8 @@ const CustomerInvoiceDetails = () => {
</Typography>
{status === InvoiceStatusTypeEnum.Draft ? (
<Chip label={translate('text_63a41a8eabb9ae67047c1bfe')} />
) : !!data?.invoice?.paymentDisputeLostAt ? (
<Status type="disputeLost" label={translate('text_66141e30699a0631f0b2ec9c')} />
) : (
<Status type={formattedStatus.type} label={translate(formattedStatus.label)} />
)}
Expand Down Expand Up @@ -527,6 +552,7 @@ const CustomerInvoiceDetails = () => {
<PremiumWarningDialog ref={premiumWarningDialogRef} />
<UpdateInvoicePaymentStatusDialog ref={updateInvoicePaymentStatusDialog} />
<VoidInvoiceDialog ref={voidInvoiceDialogRef} />
<DisputeInvoiceDialog ref={disputeInvoiceDialogRef} />
{!!data?.invoice && (
<AddMetadataDrawer ref={addMetadataDrawerDialogRef} invoice={data.invoice} />
)}
Expand Down
8 changes: 6 additions & 2 deletions src/pages/CreateCreditNote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ gql`
creditableAmountCents
refundableAmountCents
subTotalIncludingTaxesAmountCents
paymentDisputeLostAt
...InvoiceForCreditNoteFormCalculation
}
Expand Down Expand Up @@ -288,8 +289,11 @@ const CreateCreditNote = () => {
})}
</Typography>
</div>

<Status type={statusMap.type} label={translate(statusMap.label)} />
{!!invoice?.paymentDisputeLostAt ? (
<Status type="disputeLost" label={translate('text_66141e30699a0631f0b2ec9c')} />
) : (
<Status type={statusMap.type} label={translate(statusMap.label)} />
)}
</StyledCard>

<Card>
Expand Down
Loading

0 comments on commit 7486e28

Please sign in to comment.