Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate vat number #209

Open
wants to merge 10 commits into
base: development
Choose a base branch
from
178 changes: 153 additions & 25 deletions src/components/Billing/BillingDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,182 @@ import {
Space,
Select,
Fieldset,
ThemeIcon
ThemeIcon,
getPrimaryShade
} from '@mantine/core'
import { createStyles } from '@mantine/emotion'
import { useForm } from '@mantine/form'
import useAccount from 'hooks/useAccount'
import { useColorScheme } from '@mantine/hooks'
import CustomAnchor from 'components/common/customAnchor'
import { CountryNames } from 'helpers/countries'
import useAccount from 'hooks/useAccount'
import useCustomNotifications from 'hooks/useCustomNotifications'
import { useCallback } from 'react'
import CheckMarkIcon from 'resources/icons/CheckMark'
import CustomAnchor from 'components/common/customAnchor'
import { fetchService } from 'services'
import { BillingDetailsProps } from 'types'

type ValidationResult = {
isValid: boolean
error: string | null
}

enum CountryCodes {
Austria = 'AT',
Belgium = 'BE',
Bulgaria = 'BG',
Croatia = 'HR',
Cyprus = 'CY',
'Czech Republic' = 'CZ',
Denmark = 'DK',
Estonia = 'EE',
Finland = 'FI',
France = 'FR',
Germany = 'DE',
Greece = 'EL',
Hungary = 'HU',
Ireland = 'IE',
Italy = 'IT',
Latvia = 'LV',
Lithuania = 'LT',
Luxembourg = 'LU',
Malta = 'MT',
Netherlands = 'NL',
Poland = 'PL',
Portugal = 'PT',
Romania = 'RO',
Slovakia = 'SK',
Slovenia = 'SI',
Spain = 'ES',
Sweden = 'SE',
'Northern Ireland' = 'XI'
}

const useStyles = createStyles((theme) => {
const colorScheme = useColorScheme()
const primaryShade = getPrimaryShade(theme, colorScheme)
return {
container: {
backgroundColor: theme.colors.mainBackground[primaryShade],
borderRadius: theme.radius.sm,
boxShadow: theme.shadows.xs,
overflow: 'hidden',
padding: theme.spacing.lg
}
}
})

const isValidCountryCode = (code: string): code is keyof typeof CountryCodes => {
return code in CountryCodes
}

const validateVAT = async (
country: keyof typeof CountryCodes,
vat: string
): Promise<ValidationResult> => {
if (!country || !vat) return { isValid: false, error: 'Invalid input' }
const countryCode = CountryCodes[country]
let isValid = false
let error = null

try {
const response = await fetchService({
url: `https://ec.europa.eu/taxation_customs/vies/rest-api/ms/${countryCode}/vat/${vat}`
})

const result = await response.json()

if (!response.ok) {
throw new Error('Error checking VAT, try again later')
}

isValid = result.isValid
} catch (e: any) {
console.error(e.message || e)
error = e.message || e
}

return { isValid, error }
}

const BillingDetails = () => {
const { classes } = useStyles()
const {
updateBillingDetails,
adexAccount: { billingDetails }
} = useAccount()

const { showNotification } = useCustomNotifications()
const form = useForm({
initialValues: billingDetails,
validateInputOnBlur: true,
validate: {
firstName: (value: string) => {
if (value.length > 0 && value.length < 2) {
return 'First name must have at least 2 letters'
}
firstName: (value) => {
if (!value) return 'First name is required'

return null
return value.length > 0 && value.length < 2
? 'First name must have at least 2 letters'
: null
},
lastName: (value: string) => {
if (value.length > 0 && value.length < 2) {
return 'Last name must have at least 2 letters'
}
lastName: (value) => {
if (!value) return 'Last name is required'

return null
return value.length > 0 && value.length < 2
? 'Last name must have at least 2 letters'
: null
},
companyName: (value: string) =>
value.length < 2 ? 'Company name must have at least 2 characters' : null,
companyNumber: (value: string) =>
value.length === 0 ? 'Registration number is required' : null,
companyNumberPrim: () => null, // No validation for VAT number
companyAddress: (value: string) =>
value.length === 0 ? 'Company address is required' : null,
companyCountry: (value: string) => (value.length === 0 ? 'Country is required' : null),
companyCity: (value: string) => (value.length === 0 ? 'City is required' : null),
companyZipCode: (value: string) => (value.length === 0 ? 'Zip code is required' : null) // Zip code can be alphanumeric
companyName: (value) => {
if (!value) return 'Company name is required'

return value.length < 2 ? 'Company name must have at least 2 characters' : null
},
companyNumber: (value) =>
!value || value.length === 0 ? 'Registration number is required' : null,
companyNumberPrim: (value) =>
!value || value.length === 0 ? 'VAT number is required' : null,
companyAddress: (value) =>
!value || value.length === 0 ? 'Company address is required' : null,
companyCountry: (value) => (!value || value.length === 0 ? 'Country is required' : null),
companyCity: (value) => (!value || value.length === 0 ? 'City is required' : null),
companyZipCode: (value) => (!value || value.length === 0 ? 'Zip code is required' : null) // Zip code can be alphanumeric
}
})

const handleSubmit = useCallback(
async (values: BillingDetailsProps) => {
const { companyNumberPrim, companyCountry } = values

if (companyNumberPrim && companyCountry && isValidCountryCode(companyCountry)) {
try {
const { isValid, error } = await validateVAT(
companyCountry as keyof typeof CountryCodes,
companyNumberPrim
)
if (!isValid) {
form.setFieldError('companyNumberPrim', error || 'Invalid VAT number')
showNotification('error', error || 'Validating VAT number', 'Validating VAT number')
return
}
} catch (e: any) {
console.error(e)
showNotification(
'error',
'Validating VAT number',
e.message || e || 'Validating VAT number'
)
}
}

updateBillingDetails(values)
},
[updateBillingDetails, form, showNotification]
)

return (
<Fieldset disabled={billingDetails.verified}>
<form onSubmit={form.onSubmit((values) => updateBillingDetails(values))}>
<form
className={classes.container}
onSubmit={form.onSubmit((values) => handleSubmit(values))}
>
<Stack>
<Text size="sm" c="dimmed" fw="bold">
Company details
Expand Down
6 changes: 3 additions & 3 deletions src/contexts/AccountContext/AccountContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useEffect,
useState
} from 'react'
import { Account, BillingDetails, IAdExAccount } from 'types'
import { Account, BillingDetailsProps, IAdExAccount } from 'types'
import { isAdminToken, isTokenExpired, getJWTExpireTime } from 'lib/backend'
import { AmbireLoginSDK } from '@ambire/login-sdk-core'
import { DAPP_ICON_PATH, DAPP_NAME, DEFAULT_CHAIN_ID } from 'constants/login'
Expand Down Expand Up @@ -91,7 +91,7 @@ interface IAccountContext {
reqOptions: ApiRequestOptions
) => Promise<R>
updateBalance: () => Promise<void>
updateBillingDetails: (billingDetails: BillingDetails) => Promise<void>
updateBillingDetails: (billingDetails: BillingDetailsProps) => Promise<void>
isLoading: boolean
}

Expand Down Expand Up @@ -568,7 +568,7 @@ const AccountProvider: FC<PropsWithChildren> = ({ children }) => {
}, [adexServicesRequest, setAdexAccount, showNotification])

const updateBillingDetails = useCallback(
async (billingDetails: BillingDetails) => {
async (billingDetails: BillingDetailsProps) => {
try {
const updated = await adexServicesRequest<{ success?: boolean }>('backend', {
route: '/dsp/accounts/billing-details',
Expand Down
4 changes: 2 additions & 2 deletions src/types/account.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TODO: These types should get from adex-common

export interface BillingDetails {
export interface BillingDetailsProps {
firstName: string
lastName: string
companyName: string
Expand Down Expand Up @@ -66,7 +66,7 @@ export interface Account {
total: bigint
perCampaign: CampaignRefunds[]
}
billingDetails: BillingDetails
billingDetails: BillingDetailsProps
created: Date
updated: Date
info?: AccountInfo
Expand Down
4 changes: 2 additions & 2 deletions src/types/billing.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BillingDetails, Deposit, CampaignFundsActive, CampaignRefunds } from 'types'
import { BillingDetailsProps, Deposit, CampaignFundsActive, CampaignRefunds } from 'types'

export interface IInvoices {
[index: string]: any
Expand All @@ -11,7 +11,7 @@ export interface IInvoices {
amountSpent: string
}

export type InvoiceCompanyDetails = BillingDetails & {
export type InvoiceCompanyDetails = BillingDetailsProps & {
ethAddress: string
}

Expand Down