import { useLazyQuery, useMutation } from '@apollo/react-hooks' import { makeStyles, Box } from '@material-ui/core' import BigNumber from 'bignumber.js' import FileSaver from 'file-saver' import gql from 'graphql-tag' import JSZip from 'jszip' import { utils as coinUtils } from 'lamassu-coins' import moment from 'moment' 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_TRANSACTION = gql` mutation cancelCashOutTransaction($id: ID!) { cancelCashOutTransaction(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 zip = new JSZip() const [fetchSummary] = useLazyQuery(TX_SUMMARY, { onCompleted: data => createCsv(data) }) const [cancelCashOutTransaction] = useMutation(CANCEL_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 customer = tx.customerIdCardData && { name: `${onlyFirstToUpper( tx.customerIdCardData.firstName )} ${onlyFirstToUpper(tx.customerIdCardData.lastName)}`, age: moment().diff(moment(tx.customerIdCardData.dateOfBirth), 'years'), country: tx.customerIdCardData.country, idCardNumber: tx.customerIdCardData.documentNumber, idCardExpirationDate: moment(tx.customerIdCardData.expirationDate).format( 'DD-MM-YYYY' ) } const from = moment(tx.created) .subtract(MINUTES_OFFSET, 'm') .format() const until = moment(tx.created) .add(MINUTES_OFFSET, 'm') .format() 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)} ) return (
{tx.txClass === 'cashOut' ? : } {tx.txClass === 'cashOut' ? '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}
)}
{tx.txClass === 'cashIn' ? `${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 }) cancelCashOutTransaction({ variables: { id: tx.id } }) }} onDissmised={() => { setAction({ command: null }) setErrorMessage(null) }} />
) } export default memo( DetailsRow, (prev, next) => prev.it.id === next.it.id && prev.it.hasError === next.it.hasError )