feat: typesafe query and more UI changes

This commit is contained in:
Rafael Taranto 2025-05-22 11:47:50 +01:00
parent d73bb88f1d
commit 64e358f61c
13 changed files with 1347 additions and 109 deletions

View file

@ -290,7 +290,6 @@ const CustomerProfile = memo(() => {
const [error, setError] = useState(null)
const [clickedItem, setClickedItem] = useState('overview')
const { id: customerId } = useParams()
console.log(customerId)
const {
data: customerResponse,

View file

@ -1,3 +1,6 @@
import IconButton from '@mui/material/IconButton'
import Tooltip from '@mui/material/Tooltip'
import Visibility from '@mui/icons-material/Visibility'
import { format } from 'date-fns/fp'
import * as R from 'ramda'
import React, { useMemo } from 'react'
@ -40,7 +43,6 @@ const CustomersList = ({ data, country, onClick, loading }) => {
...alignRight,
},
{
id: 'totalSpent',
accessorKey: 'totalSpent',
size: 152,
enableColumnFilter: false,
@ -49,17 +51,6 @@ const CustomersList = ({ data, country, onClick, loading }) => {
header: 'Total spent',
...alignRight,
},
{
header: 'Last active',
// accessorKey: 'lastActive',
accessorFn: it => new Date(it.lastActive),
size: 133,
enableColumnFilter: false,
Cell: ({ cell }) =>
(cell.getValue() &&
format('yyyy-MM-dd', new Date(cell.getValue()))) ??
'',
},
{
header: 'Last transaction',
...alignRight,
@ -80,12 +71,34 @@ const CustomersList = ({ data, country, onClick, loading }) => {
)
},
},
{
accessorKey: 'lastActive',
header: 'Last active',
size: 133,
enableColumnFilter: false,
Cell: ({ cell }) =>
(cell.getValue() &&
format('yyyy-MM-dd', new Date(cell.getValue()))) ??
'',
},
{
header: 'Status',
id: 'status',
size: 100,
size: 150,
enableColumnFilter: false,
accessorKey: 'authorizedStatus',
sortingFn: (rowA, rowB) => {
const statusOrder = { success: 0, warning: 1, error: 2 }
const statusA = rowA.original.authorizedStatus.type
const statusB = rowB.original.authorizedStatus.type
if (statusA === statusB) {
return rowA.original.authorizedStatus.label.localeCompare(
rowB.original.authorizedStatus.label,
)
}
return statusOrder[statusA] - statusOrder[statusB]
},
Cell: ({ cell }) => <MainStatus statuses={[cell.getValue()]} />,
},
],
@ -101,13 +114,22 @@ const CustomersList = ({ data, country, onClick, loading }) => {
columnVisibility: {
id: false,
},
sorting: [{ id: 'lastActive', desc: true }],
columnPinning: { right: ['mrt-row-actions'] },
},
state: { isLoading: loading },
getRowId: it => it.id,
muiTableBodyRowProps: ({ row }) => ({
onClick: () => onClick(row),
sx: { cursor: 'pointer' },
}),
enableRowActions: true,
positionActionsColumn: 'last',
renderRowActions: ({ row }) => (
<div>
<Tooltip title="Customer page">
<IconButton aria-label="Go to customer" onClick={() => onClick(row)}>
<Visibility />
</IconButton>
</Tooltip>
</div>
),
})
return (

View file

@ -293,4 +293,31 @@ describe('getAuthorizedStatus', () => {
type: 'error',
})
})
it('should return rejected status for blocked custom info request', () => {
const customer = {
authorizedOverride: null,
isSuspended: false,
customInfoRequests: [
{
infoRequestId: '550e8400-e29b-41d4-a716-446655440000',
override: 'blocked',
},
],
}
const triggers = {
automation: 'manual',
overrides: [],
}
const customRequests = [{ id: '550e8400-e29b-41d4-a716-446655440000' }]
const result = getAuthorizedStatus(customer, triggers, customRequests)
expect(result).toEqual({
label: 'Rejected',
type: 'error',
})
})
})

View file

@ -8,7 +8,6 @@ const fs = require('fs')
const util = require('util')
const db = require('./db')
const anonymous = require('../lib/constants').anonymousCustomer
const complianceOverrides = require('./compliance_overrides')
const writeFile = util.promisify(fs.writeFile)
const notifierQueries = require('./notifier/queries')
@ -17,6 +16,7 @@ const sms = require('./sms')
const settingsLoader = require('./new-settings-loader')
const logger = require('./logger')
const externalCompliance = require('./compliance-external')
const { getCustomerList } = require('typesafe-db/lib/customers')
const { APPROVED, RETRY } = require('./plugins/compliance/consts')
@ -489,88 +489,8 @@ function getSlimCustomerByIdBatch(ids) {
return db.any(sql, [ids]).then(customers => _.map(camelize, customers))
}
// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored
/**
* Query all customers, ordered by last activity
* and with aggregate columns based on their
* transactions
*
* @returns {array} Array of customers with it's transactions aggregations
*/
function getCustomersList(
phone = null,
name = null,
address = null,
id = null,
email = null,
) {
const passableErrorCodes = _.map(
Pgp.as.text,
TX_PASSTHROUGH_ERROR_CODES,
).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
phone, email, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_data_provided, last_auth_attempt) AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
FROM (
SELECT c.id, c.authorized_override,
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
c.suspended_until > NOW() AS is_suspended,
c.front_camera_path, c.front_camera_override,
c.phone, c.email, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions, c.last_auth_attempt,
GREATEST(c.phone_at, c.email_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
row_number() OVER (partition by c.id order by t.created desc) AS rn,
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
coalesce(sum(CASE WHEN error_code IS NULL OR error_code NOT IN ($1^) THEN t.fiat ELSE 0 END) OVER (partition by c.id), 0) AS total_spent, ccf.custom_fields
FROM customers c LEFT OUTER JOIN (
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
FROM cash_in_txs WHERE send_confirmed = true OR batched = true UNION
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
FROM cash_out_txs WHERE confirmed_at IS NOT NULL) AS t ON c.id = t.customer_id
LEFT OUTER JOIN (
SELECT cf.customer_id, json_agg(json_build_object('id', cf.custom_field_id, 'label', cf.label, 'value', cf.value)) AS custom_fields FROM (
SELECT ccfp.custom_field_id, ccfp.customer_id, cfd.label, ccfp.value FROM custom_field_definitions cfd
LEFT OUTER JOIN customer_custom_field_pairs ccfp ON cfd.id = ccfp.custom_field_id
) cf GROUP BY cf.customer_id
) ccf ON c.id = ccf.customer_id
LEFT OUTER JOIN (
SELECT customer_id, coalesce(json_agg(customer_notes.*), '[]'::json) AS notes FROM customer_notes
GROUP BY customer_notes.customer_id
) cn ON c.id = cn.customer_id
WHERE c.id != $2
) AS cl WHERE rn = 1
AND ($4 IS NULL OR phone = $4)
AND ($5 IS NULL OR CONCAT(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $5 OR id_card_data::json->>'firstName' = $5 OR id_card_data::json->>'lastName' = $5)
AND ($6 IS NULL OR id_card_data::json->>'address' = $6)
AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7)
AND ($8 IS NULL OR email = $8)
ORDER BY last_active DESC
limit $3`
return db
.any(sql, [
passableErrorCodes,
anonymous.uuid,
null,
phone,
name,
address,
id,
email,
])
.then(customers =>
Promise.all(
_.map(
customer => getCustomInfoRequestsData(customer).then(camelizeDeep),
customers,
),
),
)
function getCustomersList() {
return getCustomerList({ withCustomInfoRequest: true })
}
/**

View file

@ -17,8 +17,7 @@ const resolvers = {
isAnonymous: parent => parent.customerId === anonymous.uuid,
},
Query: {
customers: (...[, { phone, email, name, address, id }]) =>
customers.getCustomersList(phone, name, address, id, email),
customers: () => customers.getCustomersList(),
customer: (...[, { customerId }]) =>
customers.getCustomerById(customerId).then(addLastUsedMachineName),
},

View file

@ -0,0 +1,20 @@
{
"name": "typesafe-db",
"version": "11.0.0-beta.0",
"license": "../LICENSE",
"type": "module",
"devDependencies": {
"kysely-codegen": "^0.18.5",
"typescript": "^5.8.3"
},
"scripts": {
"build": "tsc",
"start": "tsc --watch",
"clean": "rm -rf lib",
"generate-types": "kysely-codegen --camel-case --out-file ./src/types/types.d.ts"
},
"dependencies": {
"kysely": "^0.28.2",
"pg": "^8.16.0"
}
}

View file

@ -0,0 +1,159 @@
import db from './db.js'
import { ExpressionBuilder, sql } from 'kysely'
import { Customers, DB } from './types/types.js'
import { jsonObjectFrom } from 'kysely/helpers/postgres'
type CustomerEB = ExpressionBuilder<DB & { c: Customers }, 'c'>
const TX_PASSTHROUGH_ERROR_CODES = [
'operatorCancel',
'scoreThresholdReached',
'walletScoringError',
]
function transactionUnion(eb: CustomerEB) {
return eb
.selectFrom('cashInTxs')
.select([
'created',
'fiat',
'fiatCode',
'errorCode',
eb.val('cashIn').as('txClass'),
])
.where(({ eb, and, or, ref }) =>
and([
eb('customerId', '=', ref('c.id')),
or([eb('sendConfirmed', '=', true), eb('batched', '=', true)]),
]),
)
.unionAll(
eb
.selectFrom('cashOutTxs')
.select([
'created',
'fiat',
'fiatCode',
'errorCode',
eb.val('cashOut').as('txClass'),
])
.where(({ eb, and, ref }) =>
and([
eb('customerId', '=', ref('c.id')),
eb('confirmedAt', 'is not', null),
]),
),
)
}
function joinLatestTx(eb: CustomerEB) {
return eb
.selectFrom(eb =>
transactionUnion(eb).orderBy('created', 'desc').limit(1).as('lastTx'),
)
.select(['fiatCode', 'fiat', 'txClass', 'created'])
.as('lastTx')
}
function joinTxsTotals(eb: CustomerEB) {
return eb
.selectFrom(eb => transactionUnion(eb).as('combinedTxs'))
.select([
eb => eb.fn.coalesce(eb.fn.countAll(), eb.val(0)).as('totalTxs'),
eb =>
eb.fn
.coalesce(
eb.fn.sum(
eb
.case()
.when(
eb.or([
eb('combinedTxs.errorCode', 'is', null),
eb(
'combinedTxs.errorCode',
'not in',
TX_PASSTHROUGH_ERROR_CODES,
),
]),
)
.then(eb.ref('combinedTxs.fiat'))
.else(0)
.end(),
),
eb.val(0),
)
.as('totalSpent'),
])
.as('txStats')
}
interface GetCustomerListOptions {
withCustomInfoRequest: boolean
}
const defaultOptions: GetCustomerListOptions = {
withCustomInfoRequest: false,
}
// TODO left join lateral is having issues deriving type
function getCustomerList(
options: GetCustomerListOptions = defaultOptions,
): Promise<any[]> {
return db
.selectFrom('customers as c')
.leftJoinLateral(joinTxsTotals, join => join.onTrue())
.leftJoinLateral(joinLatestTx, join => join.onTrue())
.select(({ eb, fn, val, ref }) => [
'id',
'authorizedOverride',
'frontCameraPath',
'frontCameraOverride',
'idCardPhotoPath',
'idCardPhotoOverride',
'idCardData',
'idCardDataOverride',
'email',
'usSsn',
'usSsnOverride',
'sanctions',
'sanctionsOverride',
'txStats.totalSpent',
'txStats.totalTxs',
ref('lastTx.fiatCode').as('lastTxFiatCode'),
ref('lastTx.fiat').as('lastTxFiat'),
ref('lastTx.txClass').as('lastTxClass'),
fn<Date>('GREATEST', [
'c.created',
// 'lastTx.created',
'c.phoneAt',
'c.emailAt',
'c.idCardDataAt',
'c.frontCameraAt',
'c.idCardPhotoAt',
'c.usSsnAt',
'c.lastAuthAttempt',
]).as('lastActive'),
eb('c.suspendedUntil', '>', fn<Date>('NOW', [])).as('isSuspended'),
fn<number>('GREATEST', [
val(0),
fn<number>('date_part', [
val('day'),
eb('c.suspendedUntil', '-', fn<Date>('NOW', [])),
]),
]).as('daysSuspended'),
])
.$if(options.withCustomInfoRequest, qb =>
qb.select(({ eb, ref }) =>
jsonObjectFrom(
eb
.selectFrom('customersCustomInfoRequests')
.selectAll()
.where('customerId', '=', ref('c.id')),
).as('customInfoRequestData'),
),
)
.orderBy('lastActive', 'desc')
.execute()
}
export { getCustomerList }

View file

@ -0,0 +1,24 @@
import { DB } from './types/types.js'
import { Pool } from 'pg'
import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely'
import { PSQL_URL } from 'lamassu-server/lib/constants.js'
const dialect = new PostgresDialect({
pool: new Pool({
connectionString: PSQL_URL,
max: 5,
}),
})
export default new Kysely<DB>({
dialect,
plugins: [new CamelCasePlugin()],
log(event) {
if (event.level === 'query') {
console.log('Query:', event.query.sql)
console.log('Parameters:', event.query.parameters)
console.log('Duration:', event.queryDurationMillis + 'ms')
console.log('---')
}
},
})

View file

@ -0,0 +1,745 @@
/**
* This file was generated by kysely-codegen.
* Please do not edit it manually.
*/
import type { ColumnType } from 'kysely'
export type AuthTokenType = 'reset_password' | 'reset_twofa'
export type CashUnit =
| 'cashbox'
| 'cassette1'
| 'cassette2'
| 'cassette3'
| 'cassette4'
| 'recycler1'
| 'recycler2'
| 'recycler3'
| 'recycler4'
| 'recycler5'
| 'recycler6'
export type CashUnitOperationType =
| 'cash-box-empty'
| 'cash-box-refill'
| 'cash-cassette-1-count-change'
| 'cash-cassette-1-empty'
| 'cash-cassette-1-refill'
| 'cash-cassette-2-count-change'
| 'cash-cassette-2-empty'
| 'cash-cassette-2-refill'
| 'cash-cassette-3-count-change'
| 'cash-cassette-3-empty'
| 'cash-cassette-3-refill'
| 'cash-cassette-4-count-change'
| 'cash-cassette-4-empty'
| 'cash-cassette-4-refill'
| 'cash-recycler-1-count-change'
| 'cash-recycler-1-empty'
| 'cash-recycler-1-refill'
| 'cash-recycler-2-count-change'
| 'cash-recycler-2-empty'
| 'cash-recycler-2-refill'
| 'cash-recycler-3-count-change'
| 'cash-recycler-3-empty'
| 'cash-recycler-3-refill'
| 'cash-recycler-4-count-change'
| 'cash-recycler-4-empty'
| 'cash-recycler-4-refill'
| 'cash-recycler-5-count-change'
| 'cash-recycler-5-empty'
| 'cash-recycler-5-refill'
| 'cash-recycler-6-count-change'
| 'cash-recycler-6-empty'
| 'cash-recycler-6-refill'
export type ComplianceType =
| 'authorized'
| 'front_camera'
| 'hard_limit'
| 'id_card_data'
| 'id_card_photo'
| 'sanctions'
| 'sms'
| 'us_ssn'
export type DiscountSource = 'individualDiscount' | 'promoCode'
export type ExternalComplianceStatus =
| 'APPROVED'
| 'PENDING'
| 'REJECTED'
| 'RETRY'
export type Generated<T> =
T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>
export type Int8 = ColumnType<
string,
bigint | number | string,
bigint | number | string
>
export type Json = JsonValue
export type JsonArray = JsonValue[]
export type JsonObject = {
[x: string]: JsonValue | undefined
}
export type JsonPrimitive = boolean | number | string | null
export type JsonValue = JsonArray | JsonObject | JsonPrimitive
export type NotificationType =
| 'compliance'
| 'cryptoBalance'
| 'error'
| 'fiatBalance'
| 'highValueTransaction'
| 'security'
| 'transaction'
export type Numeric = ColumnType<string, number | string, number | string>
export type Role = 'superuser' | 'user'
export type SmsNoticeEvent =
| 'cash_out_dispense_ready'
| 'sms_code'
| 'sms_receipt'
export type StatusStage =
| 'authorized'
| 'confirmed'
| 'instant'
| 'insufficientFunds'
| 'notSeen'
| 'published'
| 'rejected'
export type Timestamp = ColumnType<Date, Date | string, Date | string>
export type TradeType = 'buy' | 'sell'
export type TransactionBatchStatus = 'failed' | 'open' | 'ready' | 'sent'
export type VerificationType = 'automatic' | 'blocked' | 'verified'
export interface AuthTokens {
expire: Generated<Timestamp>
token: string
type: AuthTokenType
userId: string | null
}
export interface Bills {
cashboxBatchId: string | null
cashInFee: Numeric
cashInTxsId: string
created: Generated<Timestamp>
cryptoCode: Generated<string | null>
destinationUnit: Generated<CashUnit>
deviceTime: Int8
fiat: number
fiatCode: string
id: string
legacy: Generated<boolean | null>
}
export interface Blacklist {
address: string
blacklistMessageId: Generated<string>
}
export interface BlacklistMessages {
allowToggle: Generated<boolean>
content: string
id: string
label: string
}
export interface CashInActions {
action: string
created: Generated<Timestamp>
error: string | null
errorCode: string | null
id: Generated<number>
txHash: string | null
txId: string
}
export interface CashInTxs {
batched: Generated<boolean>
batchId: string | null
batchTime: Timestamp | null
cashInFee: Numeric
commissionPercentage: Generated<Numeric | null>
created: Generated<Timestamp>
cryptoAtoms: Numeric
cryptoCode: string
customerId: Generated<string | null>
deviceId: string
discount: number | null
discountSource: DiscountSource | null
email: string | null
error: string | null
errorCode: string | null
fee: Int8 | null
fiat: Numeric
fiatCode: string
id: string
isPaperWallet: Generated<boolean | null>
minimumTx: number
operatorCompleted: Generated<boolean>
phone: string | null
rawTickerPrice: Generated<Numeric | null>
send: Generated<boolean>
sendConfirmed: Generated<boolean>
sendPending: Generated<boolean>
sendTime: Timestamp | null
termsAccepted: Generated<boolean>
timedout: Generated<boolean>
toAddress: string
txCustomerPhotoAt: Timestamp | null
txCustomerPhotoPath: string | null
txHash: string | null
txVersion: number
walletScore: number | null
}
export interface CashinTxTrades {
tradeId: Generated<number>
txId: string
}
export interface CashOutActions {
action: string
created: Generated<Timestamp>
denomination1: number | null
denomination2: number | null
denomination3: number | null
denomination4: number | null
denominationRecycler1: number | null
denominationRecycler2: number | null
denominationRecycler3: number | null
denominationRecycler4: number | null
denominationRecycler5: number | null
denominationRecycler6: number | null
deviceId: Generated<string>
deviceTime: Int8 | null
dispensed1: number | null
dispensed2: number | null
dispensed3: number | null
dispensed4: number | null
dispensedRecycler1: number | null
dispensedRecycler2: number | null
dispensedRecycler3: number | null
dispensedRecycler4: number | null
dispensedRecycler5: number | null
dispensedRecycler6: number | null
error: string | null
errorCode: string | null
id: Generated<number>
layer2Address: string | null
provisioned1: number | null
provisioned2: number | null
provisioned3: number | null
provisioned4: number | null
provisionedRecycler1: number | null
provisionedRecycler2: number | null
provisionedRecycler3: number | null
provisionedRecycler4: number | null
provisionedRecycler5: number | null
provisionedRecycler6: number | null
redeem: Generated<boolean>
rejected1: number | null
rejected2: number | null
rejected3: number | null
rejected4: number | null
rejectedRecycler1: number | null
rejectedRecycler2: number | null
rejectedRecycler3: number | null
rejectedRecycler4: number | null
rejectedRecycler5: number | null
rejectedRecycler6: number | null
toAddress: string | null
txHash: string | null
txId: string
}
export interface CashOutTxs {
commissionPercentage: Generated<Numeric | null>
confirmedAt: Timestamp | null
created: Generated<Timestamp>
cryptoAtoms: Numeric
cryptoCode: string
customerId: Generated<string | null>
denomination1: number | null
denomination2: number | null
denomination3: number | null
denomination4: number | null
denominationRecycler1: number | null
denominationRecycler2: number | null
denominationRecycler3: number | null
denominationRecycler4: number | null
denominationRecycler5: number | null
denominationRecycler6: number | null
deviceId: string
discount: number | null
discountSource: DiscountSource | null
dispense: Generated<boolean>
dispenseConfirmed: Generated<boolean | null>
email: string | null
error: string | null
errorCode: string | null
fiat: Numeric
fiatCode: string
fixedFee: Generated<Numeric>
hdIndex: Generated<number | null>
id: string
layer2Address: string | null
notified: Generated<boolean>
phone: string | null
provisioned1: number | null
provisioned2: number | null
provisioned3: number | null
provisioned4: number | null
provisionedRecycler1: number | null
provisionedRecycler2: number | null
provisionedRecycler3: number | null
provisionedRecycler4: number | null
provisionedRecycler5: number | null
provisionedRecycler6: number | null
publishedAt: Timestamp | null
rawTickerPrice: Generated<Numeric | null>
receivedCryptoAtoms: Generated<Numeric | null>
redeem: Generated<boolean>
status: Generated<StatusStage>
swept: Generated<boolean>
termsAccepted: Generated<boolean>
timedout: Generated<boolean>
toAddress: string
txCustomerPhotoAt: Timestamp | null
txCustomerPhotoPath: string | null
txVersion: number
walletScore: number | null
}
export interface CashoutTxTrades {
tradeId: Generated<number>
txId: string
}
export interface CashUnitOperation {
billCountOverride: number | null
created: Generated<Timestamp>
deviceId: string | null
id: string
operationType: CashUnitOperationType
performedBy: string | null
}
export interface ComplianceOverrides {
complianceType: ComplianceType
customerId: string | null
id: string
overrideAt: Timestamp
overrideBy: string | null
verification: VerificationType
}
export interface Coupons {
code: string
discount: number
id: string
softDeleted: Generated<boolean | null>
}
export interface CustomerCustomFieldPairs {
customerId: string
customFieldId: string
value: string
}
export interface CustomerExternalCompliance {
customerId: string
externalId: string
lastKnownStatus: ExternalComplianceStatus | null
lastUpdated: Generated<Timestamp>
service: string
}
export interface CustomerNotes {
content: Generated<string>
created: Generated<Timestamp>
customerId: string
id: string
lastEditedAt: Timestamp | null
lastEditedBy: string | null
title: Generated<string>
}
export interface Customers {
address: string | null
authorizedAt: Timestamp | null
authorizedOverride: Generated<VerificationType>
authorizedOverrideAt: Timestamp | null
authorizedOverrideBy: string | null
created: Generated<Timestamp>
email: string | null
emailAt: Timestamp | null
frontCameraAt: Timestamp | null
frontCameraOverride: Generated<VerificationType>
frontCameraOverrideAt: Timestamp | null
frontCameraOverrideBy: string | null
frontCameraPath: string | null
id: string
idCardData: Json | null
idCardDataAt: Timestamp | null
idCardDataExpiration: Timestamp | null
idCardDataNumber: string | null
idCardDataOverride: Generated<VerificationType>
idCardDataOverrideAt: Timestamp | null
idCardDataOverrideBy: string | null
idCardDataRaw: string | null
idCardPhotoAt: Timestamp | null
idCardPhotoOverride: Generated<VerificationType>
idCardPhotoOverrideAt: Timestamp | null
idCardPhotoOverrideBy: string | null
idCardPhotoPath: string | null
isTestCustomer: Generated<boolean>
lastAuthAttempt: Timestamp | null
lastUsedMachine: string | null
name: string | null
phone: string | null
phoneAt: Timestamp | null
phoneOverride: Generated<VerificationType>
phoneOverrideAt: Timestamp | null
phoneOverrideBy: string | null
sanctions: boolean | null
sanctionsAt: Timestamp | null
sanctionsOverride: Generated<VerificationType>
sanctionsOverrideAt: Timestamp | null
sanctionsOverrideBy: string | null
smsOverride: Generated<VerificationType>
smsOverrideAt: Timestamp | null
smsOverrideBy: string | null
subscriberInfo: Json | null
subscriberInfoAt: Timestamp | null
subscriberInfoBy: string | null
suspendedUntil: Timestamp | null
usSsn: string | null
usSsnAt: Timestamp | null
usSsnOverride: Generated<VerificationType>
usSsnOverrideAt: Timestamp | null
usSsnOverrideBy: string | null
}
export interface CustomersCustomInfoRequests {
customerData: Json
customerId: string
infoRequestId: string
override: Generated<VerificationType>
overrideAt: Timestamp | null
overrideBy: string | null
}
export interface CustomFieldDefinitions {
active: Generated<boolean | null>
id: string
label: string
}
export interface CustomInfoRequests {
customRequest: Json | null
enabled: Generated<boolean>
id: string
}
export interface Devices {
cassette1: Generated<number>
cassette2: Generated<number>
cassette3: Generated<number>
cassette4: Generated<number>
created: Generated<Timestamp>
deviceId: string
diagnosticsFrontUpdatedAt: Timestamp | null
diagnosticsScanUpdatedAt: Timestamp | null
diagnosticsTimestamp: Timestamp | null
display: Generated<boolean>
lastOnline: Generated<Timestamp>
location: Generated<Json>
model: string | null
name: string
numberOfCassettes: Generated<number>
numberOfRecyclers: Generated<number>
paired: Generated<boolean>
recycler1: Generated<number>
recycler2: Generated<number>
recycler3: Generated<number>
recycler4: Generated<number>
recycler5: Generated<number>
recycler6: Generated<number>
userConfigId: number | null
version: string | null
}
export interface EditedCustomerData {
created: Generated<Timestamp>
customerId: string
frontCameraAt: Timestamp | null
frontCameraBy: string | null
frontCameraPath: string | null
idCardData: Json | null
idCardDataAt: Timestamp | null
idCardDataBy: string | null
idCardPhotoAt: Timestamp | null
idCardPhotoBy: string | null
idCardPhotoPath: string | null
name: string | null
nameAt: Timestamp | null
nameBy: string | null
subscriberInfo: Json | null
subscriberInfoAt: Timestamp | null
subscriberInfoBy: string | null
usSsn: string | null
usSsnAt: Timestamp | null
usSsnBy: string | null
}
export interface EmptyUnitBills {
cashboxBatchId: string | null
created: Generated<Timestamp>
deviceId: string
fiat: number
fiatCode: string
id: string
}
export interface HardwareCredentials {
created: Generated<Timestamp | null>
data: Json
id: string
lastUsed: Generated<Timestamp | null>
userId: string
}
export interface IndividualDiscounts {
customerId: string
discount: number
id: string
softDeleted: Generated<boolean | null>
}
export interface Logs {
deviceId: string | null
id: string
logLevel: string | null
message: string | null
serial: Generated<number>
serverTimestamp: Generated<Timestamp>
timestamp: Timestamp | null
}
export interface MachineEvents {
created: Generated<Timestamp>
deviceId: string
deviceTime: Timestamp | null
eventType: string
id: string
note: string | null
}
export interface MachineNetworkHeartbeat {
averagePacketLoss: Numeric
averageResponseTime: Numeric
created: Generated<Timestamp>
deviceId: string
id: string
}
export interface MachineNetworkPerformance {
created: Generated<Timestamp>
deviceId: string
downloadSpeed: Numeric
}
export interface MachinePings {
deviceId: string
deviceTime: Timestamp
updated: Generated<Timestamp>
}
export interface Migrations {
data: Json
id: Generated<number>
}
export interface Notifications {
created: Generated<Timestamp>
detail: Json | null
id: string
message: string
read: Generated<boolean>
type: NotificationType
valid: Generated<boolean>
}
export interface OperatorIds {
id: Generated<number>
operatorId: string
service: string
}
export interface PairingTokens {
created: Generated<Timestamp>
id: Generated<number>
name: string
token: string | null
}
export interface SanctionsLogs {
created: Generated<Timestamp>
customerId: string
deviceId: string
id: string
sanctionedAliasFullName: string
sanctionedAliasId: string | null
sanctionedId: string
}
export interface ServerLogs {
deviceId: string | null
id: string
logLevel: string | null
message: string | null
meta: Json | null
timestamp: Generated<Timestamp | null>
}
export interface SmsNotices {
allowToggle: Generated<boolean>
created: Generated<Timestamp>
enabled: Generated<boolean>
event: SmsNoticeEvent
id: string
message: string
messageName: string
}
export interface Trades {
created: Generated<Timestamp>
cryptoAtoms: Numeric
cryptoCode: string
error: string | null
fiatCode: string
id: Generated<number>
type: TradeType
}
export interface TransactionBatches {
closedAt: Timestamp | null
createdAt: Generated<Timestamp>
cryptoCode: string
errorMessage: string | null
id: string
status: Generated<TransactionBatchStatus>
}
export interface UnpairedDevices {
deviceId: string
id: string
model: string | null
name: string | null
paired: Timestamp
unpaired: Timestamp
}
export interface UserConfig {
created: Generated<Timestamp>
data: Json
id: Generated<number>
schemaVersion: Generated<number>
type: string
valid: boolean
}
export interface UserRegisterTokens {
expire: Generated<Timestamp>
role: Generated<Role | null>
token: string
useFido: Generated<boolean | null>
username: string
}
export interface Users {
created: Generated<Timestamp>
enabled: Generated<boolean | null>
id: string
lastAccessed: Generated<Timestamp>
lastAccessedAddress: string | null
lastAccessedFrom: string | null
password: string | null
role: Generated<Role>
tempTwofaCode: string | null
twofaCode: string | null
username: string
}
export interface UserSessions {
expire: Timestamp
sess: Json
sid: string
}
export interface DB {
authTokens: AuthTokens
bills: Bills
blacklist: Blacklist
blacklistMessages: BlacklistMessages
cashInActions: CashInActions
cashInTxs: CashInTxs
cashinTxTrades: CashinTxTrades
cashOutActions: CashOutActions
cashOutTxs: CashOutTxs
cashoutTxTrades: CashoutTxTrades
cashUnitOperation: CashUnitOperation
complianceOverrides: ComplianceOverrides
coupons: Coupons
customerCustomFieldPairs: CustomerCustomFieldPairs
customerExternalCompliance: CustomerExternalCompliance
customerNotes: CustomerNotes
customers: Customers
customersCustomInfoRequests: CustomersCustomInfoRequests
customFieldDefinitions: CustomFieldDefinitions
customInfoRequests: CustomInfoRequests
devices: Devices
editedCustomerData: EditedCustomerData
emptyUnitBills: EmptyUnitBills
hardwareCredentials: HardwareCredentials
individualDiscounts: IndividualDiscounts
logs: Logs
machineEvents: MachineEvents
machineNetworkHeartbeat: MachineNetworkHeartbeat
machineNetworkPerformance: MachineNetworkPerformance
machinePings: MachinePings
migrations: Migrations
notifications: Notifications
operatorIds: OperatorIds
pairingTokens: PairingTokens
sanctionsLogs: SanctionsLogs
serverLogs: ServerLogs
smsNotices: SmsNotices
trades: Trades
transactionBatches: TransactionBatches
unpairedDevices: UnpairedDevices
userConfig: UserConfig
userRegisterTokens: UserRegisterTokens
users: Users
userSessions: UserSessions
}

View file

@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 22",
"_version": "22.0.0",
"compilerOptions": {
"lib": ["es2023"],
"module": "nodenext",
"target": "es2022",
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node16",
"outDir": "./lib",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["lib", "node_modules"]
}