Skip to content

Commit

Permalink
chore(test) extract and test CreditNoteFormCalculation's calculation …
Browse files Browse the repository at this point in the history
…method
  • Loading branch information
ansmonjol committed Sep 6, 2023
1 parent 5ca3327 commit 39110ac
Show file tree
Hide file tree
Showing 4 changed files with 594 additions and 267 deletions.
288 changes: 21 additions & 267 deletions src/components/creditNote/CreditNoteFormCalculation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,8 @@ import { Typography, Button, Tooltip, Alert, Icon } from '~/components/designSys
import { theme } from '~/styles'
import { deserializeAmount, getCurrencyPrecision } from '~/core/serializers/serializeAmount'

import {
CreditNoteForm,
GroupedFee,
FromFee,
CreditTypeEnum,
PayBackErrorEnum,
FeesPerInvoice,
} from './types'
import { CreditNoteForm, CreditTypeEnum, PayBackErrorEnum } from './types'
import { creditNoteFormCalculationCalculation } from './utils'

gql`
fragment CreditNoteForm on Invoice {
Expand All @@ -51,15 +45,6 @@ gql`
}
`

type TaxMapType = Map<
string, // id of the tax
{
label: string
amount: number
taxRate: number // Used for sorting purpose
}
>

interface CreditNoteFormCalculationProps {
invoice?: CreditNoteFormFragment
formikProps: FormikProps<Partial<CreditNoteForm>>
Expand All @@ -76,256 +61,25 @@ export const CreditNoteFormCalculation = ({
const currencyPrecision = getCurrencyPrecision(currency)
const isLegacyInvoice = (invoice?.versionNumber || 0) < 3

// This method calculate the credit notes amounts to display
// It does parse once all items. If no coupon applied, values are used for display
// If coupon applied, it will calculate the credit note tax amount based on the coupon value on pro rata of each item
const calculation = useMemo(() => {
if (hasFeeError) return { totalExcludedTax: undefined, taxes: new Map() }

const mergeTaxMaps = (map1: TaxMapType, map2: TaxMapType): TaxMapType => {
if (!map1.size) return map2
if (!map2.size) return map1

// We assume both map1 and map2 are the same length and contain the same keys
const mergedMap = new Map()

map1.forEach((_, key) => {
const previousTax1 = map1.get(key)
const previousTax2 = map2.get(key)

if (previousTax1 && previousTax2) {
mergedMap.set(key, {
label: previousTax1.label,
amount: previousTax1.amount + previousTax2.amount,
taxRate: previousTax1.taxRate,
})
}
})

return mergedMap
}

const updateOrCreateTaxMap = (
currentTaxesMap: TaxMapType,
feeAmount?: number,
feeAppliedTaxes?: { id: string; tax: { id: string; name: string; rate: number } }[]
) => {
if (!feeAppliedTaxes?.length) return currentTaxesMap
if (!currentTaxesMap) currentTaxesMap = new Map()

feeAppliedTaxes.forEach((appliedTax) => {
const { id, name, rate } = appliedTax.tax
const amount = ((feeAmount || 0) * rate) / 100

const previousTax = currentTaxesMap?.get(id)

if (previousTax) {
previousTax.amount += amount
currentTaxesMap?.set(id, previousTax)
} else {
currentTaxesMap?.set(id, { amount, label: `${name} (${rate}%)`, taxRate: rate })
}
})

return currentTaxesMap
}

const feeTotal = Object.keys(formikProps?.values.fees || {}).reduce<{
totalExcludedTax: number
taxes: TaxMapType
}>(
(accSub, subKey) => {
const subChild = ((formikProps?.values.fees as FeesPerInvoice) || {})[subKey]
const subValues = Object.keys(subChild?.fees || {}).reduce<{
totalExcludedTax: number
taxes: TaxMapType
}>(
(accGroup, groupKey) => {
const child = subChild?.fees[groupKey] as FromFee

if (typeof child.checked === 'boolean') {
const childExcludedTax = Number(child.value as number)

return !child.checked
? accGroup
: (accGroup = {
totalExcludedTax: accGroup.totalExcludedTax + childExcludedTax,
taxes: updateOrCreateTaxMap(
accGroup.taxes,
childExcludedTax,
child?.appliedTaxes
),
})
}

const grouped = (child as unknown as GroupedFee)?.grouped
const groupedValues = Object.keys(grouped || {}).reduce<{
totalExcludedTax: number
taxes: TaxMapType
}>(
(accFee, feeKey) => {
const fee = grouped[feeKey]
const feeExcludedTax = Number(fee.value)

return !fee.checked
? accFee
: (accFee = {
totalExcludedTax: accFee.totalExcludedTax + feeExcludedTax,
taxes: updateOrCreateTaxMap(accFee.taxes, feeExcludedTax, fee?.appliedTaxes),
})
},
{ totalExcludedTax: 0, taxes: new Map() }
)

return {
totalExcludedTax: accGroup.totalExcludedTax + groupedValues.totalExcludedTax,
taxes: mergeTaxMaps(accGroup.taxes, groupedValues.taxes),
}
},
{ totalExcludedTax: 0, taxes: new Map() }
)

return {
totalExcludedTax: accSub?.totalExcludedTax + subValues.totalExcludedTax,
taxes: mergeTaxMaps(accSub?.taxes, subValues.taxes),
}
},
{ totalExcludedTax: 0, taxes: new Map() }
)

const { value: addOnValue, taxes: addOnTaxes } = formikProps.values.addOnFee?.reduce(
(acc, fee) => {
return {
value: acc.value + (fee.checked ? Number(fee.value) : 0),
taxes: updateOrCreateTaxMap(
acc.taxes,
fee.checked ? Number(fee.value) : 0,
fee?.appliedTaxes
),
}
},
{ value: 0, taxes: new Map() }
) || { value: 0, taxes: new Map() }

let proRatedCouponAmount = 0
let totalExcludedTax = feeTotal.totalExcludedTax + Number(addOnValue || 0)
const totalInvoiceFeesCreditableAmountCentsExcludingTax = Number(invoice?.feesAmountCents || 0)

// If legacy invoice or no coupon, return "basic" calculation
if (isLegacyInvoice || Number(invoice?.couponsAmountCents) === 0) {
return {
proRatedCouponAmount,
totalExcludedTax,
taxes: mergeTaxMaps(feeTotal.taxes, addOnTaxes),
}
}

const couponsAdjustmentAmountCents = () => {
return (
(Number(invoice?.couponsAmountCents) / totalInvoiceFeesCreditableAmountCentsExcludingTax) *
feeTotal.totalExcludedTax
)
}

// Parse fees a second time to calculate pro-rated amounts
const proRatedTotal = () => {
return Object.keys(formikProps?.values.fees || {}).reduce<{
totalExcludedTax: number
taxes: TaxMapType
}>(
(accSub, subKey) => {
const subChild = ((formikProps?.values.fees as FeesPerInvoice) || {})[subKey]
const subValues = Object.keys(subChild?.fees || {}).reduce<{
totalExcludedTax: number
taxes: TaxMapType
}>(
(accGroup, groupKey) => {
const child = subChild?.fees[groupKey] as FromFee

if (typeof child.checked === 'boolean') {
const childExcludedTax = Number(child.value as number)
let itemRate = Number(child.value) / feeTotal.totalExcludedTax
let proratedCouponAmount = couponsAdjustmentAmountCents() * itemRate

return !child.checked
? accGroup
: (accGroup = {
totalExcludedTax: accGroup.totalExcludedTax + childExcludedTax,
taxes: updateOrCreateTaxMap(
accGroup.taxes,
childExcludedTax - proratedCouponAmount,
child?.appliedTaxes
),
})
}

const grouped = (child as unknown as GroupedFee)?.grouped
const groupedValues = Object.keys(grouped || {}).reduce<{
totalExcludedTax: number
taxes: TaxMapType
}>(
(accFee, feeKey) => {
const fee = grouped[feeKey]
const feeExcludedTax = Number(fee.value)
let itemRate = Number(fee.value) / feeTotal.totalExcludedTax
let proratedCouponAmount = couponsAdjustmentAmountCents() * itemRate

return !fee.checked
? accFee
: (accFee = {
totalExcludedTax: accFee.totalExcludedTax + feeExcludedTax,
taxes: updateOrCreateTaxMap(
accFee.taxes,
feeExcludedTax - proratedCouponAmount,
fee?.appliedTaxes
),
})
},
{ totalExcludedTax: 0, taxes: new Map() }
)

return {
totalExcludedTax: accGroup.totalExcludedTax + groupedValues.totalExcludedTax,
taxes: mergeTaxMaps(accGroup.taxes, groupedValues.taxes),
}
},
{ totalExcludedTax: 0, taxes: new Map() }
)

return {
totalExcludedTax: accSub?.totalExcludedTax + subValues.totalExcludedTax,
taxes: mergeTaxMaps(accSub?.taxes, subValues.taxes),
}
},
{ totalExcludedTax: 0, taxes: new Map() }
)
}

// If coupon is applied, we need to pro-rate the coupon amount and the tax amount
proRatedCouponAmount =
(Number(invoice?.couponsAmountCents) / totalInvoiceFeesCreditableAmountCentsExcludingTax) *
feeTotal.totalExcludedTax

// And deduct the coupon amount from the total excluding Tax
totalExcludedTax -= proRatedCouponAmount

const { taxes } = proRatedTotal()

return {
proRatedCouponAmount,
totalExcludedTax,
taxes,
}
}, [
formikProps?.values.fees,
formikProps.values.addOnFee,
hasFeeError,
invoice?.feesAmountCents,
invoice?.couponsAmountCents,
isLegacyInvoice,
])

const { totalExcludedTax, taxes, proRatedCouponAmount } = calculation
const { totalExcludedTax, taxes, proRatedCouponAmount } = useMemo(
() =>
creditNoteFormCalculationCalculation({
hasFeeError,
isLegacyInvoice,
addOnFee: formikProps.values.addOnFee,
couponsAmountCents: invoice?.couponsAmountCents,
fees: formikProps.values.fees,
feesAmountCents: invoice?.feesAmountCents,
}),
[
formikProps.values.addOnFee,
formikProps.values.fees,
hasFeeError,
invoice?.couponsAmountCents,
invoice?.feesAmountCents,
isLegacyInvoice,
]
)
const totalTaxAmount = taxes?.size
? Array.from(taxes.values()).reduce((acc, tax) => acc + tax.amount, 0)
: 0
Expand Down
Loading

0 comments on commit 39110ac

Please sign in to comment.