import { useLazyQuery, useMutation } from '@apollo/react-hooks' import { makeStyles, Box } from '@material-ui/core' import BigNumber from 'bignumber.js' import { add, differenceInYears, format, sub, parse } from 'date-fns/fp' import FileSaver from 'file-saver' import gql from 'graphql-tag' import JSZip from 'jszip' import { utils as coinUtils } from 'lamassu-coins' import * as R from 'ramda' import React, { memo, useState } from 'react' import { ConfirmDialog } from 'src/components/ConfirmDialog' import { HoverableTooltip } from 'src/components/Tooltip' import { IDButton, ActionButton } from 'src/components/buttons' import { P, Label1 } from 'src/components/typography' import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg' import { ReactComponent as CardIdIcon } from 'src/styling/icons/ID/card/zodiac.svg' import { ReactComponent as PhoneIdInverseIcon } from 'src/styling/icons/ID/phone/white.svg' import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg' import { ReactComponent as CamIdInverseIcon } from 'src/styling/icons/ID/photo/white.svg' import { ReactComponent as CamIdIcon } from 'src/styling/icons/ID/photo/zodiac.svg' import { ReactComponent as CancelInverseIcon } from 'src/styling/icons/button/cancel/white.svg' import { ReactComponent as CancelIcon } from 'src/styling/icons/button/cancel/zodiac.svg' import { ReactComponent as DownloadInverseIcon } from 'src/styling/icons/button/download/white.svg' import { ReactComponent as Download } from 'src/styling/icons/button/download/zodiac.svg' import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg' import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg' import { URI } from 'src/utils/apollo' import { onlyFirstToUpper } from 'src/utils/string' import CopyToClipboard from './CopyToClipboard' import styles from './DetailsCard.styles' import { getStatus, getStatusDetails } from './helper' const useStyles = makeStyles(styles) const MINUTES_OFFSET = 3 const TX_SUMMARY = gql` query txSummaryAndLogs( $txId: ID! $deviceId: ID! $limit: Int $from: Date $until: Date $txClass: String $timezone: String ) { serverLogsCsv( limit: $limit from: $from until: $until timezone: $timezone ) machineLogsCsv( deviceId: $deviceId limit: $limit from: $from until: $until timezone: $timezone ) transactionCsv(id: $txId, txClass: $txClass, timezone: $timezone) txAssociatedDataCsv(id: $txId, txClass: $txClass, timezone: $timezone) } ` const CANCEL_CASH_OUT_TRANSACTION = gql` mutation cancelCashOutTransaction($id: ID!) { cancelCashOutTransaction(id: $id) { id } } ` const CANCEL_CASH_IN_TRANSACTION = gql` mutation cancelCashInTransaction($id: ID!) { cancelCashInTransaction(id: $id) { id } } ` const formatAddress = (cryptoCode = '', address = '') => coinUtils.formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ') const Label = ({ children }) => { const classes = useStyles() return {children} } const DetailsRow = ({ it: tx, timezone }) => { const classes = useStyles() const [action, setAction] = useState({ command: null }) const [errorMessage, setErrorMessage] = useState('') const isCashIn = tx.txClass === 'cashIn' const zip = new JSZip() const [fetchSummary] = useLazyQuery(TX_SUMMARY, { onCompleted: data => createCsv(data) }) const [cancelTransaction] = useMutation( isCashIn ? CANCEL_CASH_IN_TRANSACTION : CANCEL_CASH_OUT_TRANSACTION, { onError: ({ message }) => setErrorMessage(message ?? 'An error occurred.'), refetchQueries: () => ['transactions'] } ) const fiat = Number.parseFloat(tx.fiat) const crypto = coinUtils.toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode) const commissionPercentage = Number.parseFloat(tx.commissionPercentage, 2) const commission = Number(fiat * commissionPercentage).toFixed(2) const discount = tx.discount ? `-${tx.discount}%` : null const exchangeRate = BigNumber(fiat / crypto).toFormat(2) const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}` const parseDateString = parse(new Date(), 'yyyyMMdd') const customer = tx.customerIdCardData && { name: `${onlyFirstToUpper( tx.customerIdCardData.firstName )} ${onlyFirstToUpper(tx.customerIdCardData.lastName)}`, age: (tx.customerIdCardData.dateOfBirth && differenceInYears( parseDateString(tx.customerIdCardData.dateOfBirth), new Date() )) ?? '', country: tx.customerIdCardData.country, idCardNumber: tx.customerIdCardData.documentNumber, idCardExpirationDate: (tx.customerIdCardData.expirationDate && format('yyyy-MM-dd')( parseDateString(tx.customerIdCardData.expirationDate) )) ?? '' } const from = sub({ minutes: MINUTES_OFFSET }, tx.created) const until = add({ minutes: MINUTES_OFFSET }, tx.created) const downloadRawLogs = ({ id: txId, deviceId, txClass }, timezone) => { fetchSummary({ variables: { txId, from, until, deviceId, txClass, timezone } }) } const createCsv = async logs => { const zipFilename = `tx_${tx.id}_summary.zip` const filesNames = R.keys(logs) R.map(name => zip.file(name + '.csv', logs[name]), filesNames) const content = await zip.generateAsync({ type: 'blob' }) FileSaver.saveAs(content, zipFilename) } const errorElements = ( <> {getStatus(tx)} ) const getCancelMessage = () => { const cashInMessage = `The user will not be able to redeem the inserted bills, even if they subsequently confirm the transaction. If they've already deposited bills, you'll need to reconcile this transaction with them manually.` const cashOutMessage = `The user will not be able to redeem the cash, even if they subsequently send the required coins. If they've already sent you coins, you'll need to reconcile this transaction with them manually.` return isCashIn ? cashInMessage : cashOutMessage } return (
{!isCashIn ? : } {!isCashIn ? 'Cash-out' : 'Cash-in'}
{tx.customerPhone && ( {tx.customerPhone} )} {tx.customerIdCardPhotoPath && !tx.customerIdCardData && ( )} {tx.customerIdCardData && (
{customer.name}
{customer.age}
{customer.country}
{customer.idCardNumber}
{customer.idCardExpirationDate}
)} {tx.customerFrontCameraPath && ( )}
{crypto > 0 ? displayExRate : '-'}
{`${commission} ${tx.fiatCode} (${commissionPercentage * 100} %)`} {discount && (
{discount}
)}
{isCashIn ? `${Number.parseFloat(tx.cashInFee)} ${tx.fiatCode}` : 'N/A'}
{formatAddress(tx.cryptoCode, tx.toAddress)}
{tx.txClass === 'cashOut' ? ( 'N/A' ) : ( {tx.txHash} )}
{tx.id}
{getStatusDetails(tx) ? (

{getStatusDetails(tx)}

) : ( errorElements )} {tx.txClass === 'cashOut' && getStatus(tx) === 'Pending' && ( setAction({ command: 'cancelTx' }) }> Cancel transaction )}
downloadRawLogs(tx, timezone)}> Download raw logs
{ setErrorMessage(null) setAction({ command: null }) cancelTransaction({ variables: { id: tx.id } }) }} onDismissed={() => { setAction({ command: null }) setErrorMessage(null) }} />
) } export default memo( DetailsRow, (prev, next) => prev.it.id === next.it.id && prev.it.hasError === next.it.hasError && getStatus(prev.it) === getStatus(next.it) )