diff --git a/.gitignore b/.gitignore
index 8bf015e1..ad48bf9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
packages/server/certs/
packages/server/tests/stress/machines
packages/server/tests/stress/config.json
+packages/typesafe-db/lib/
diff --git a/package-lock.json b/package-lock.json
index 380af821..d9ab6abe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"license": "./LICENSE",
"workspaces": [
"packages/server",
- "packages/admin-ui"
+ "packages/admin-ui",
+ "packages/typesafe-db"
],
"devDependencies": {
"@eslint/css": "^0.7.0",
@@ -10688,6 +10689,16 @@
"wrappy": "1"
}
},
+ "node_modules/diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/diff-sequences": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz",
@@ -10844,6 +10855,22 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/dotenv-expand": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.2.tgz",
+ "integrity": "sha512-lXpXz2ZE1cea1gL4sz2Ipj8y4PiVjytYr3Ij0SWoms1PGxIv7m2CRKuRuCRtHdVuvM/hNJPMxt5PbhboNC4dPQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dotenv": "^16.4.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/downshift": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/downshift/-/downshift-9.0.9.tgz",
@@ -11054,6 +11081,16 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
@@ -13465,6 +13502,101 @@
"assert-plus": "^1.0.0"
}
},
+ "node_modules/git-diff": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/git-diff/-/git-diff-2.0.6.tgz",
+ "integrity": "sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chalk": "^2.3.2",
+ "diff": "^3.5.0",
+ "loglevel": "^1.6.1",
+ "shelljs": "^0.8.1",
+ "shelljs.exec": "^1.1.7"
+ },
+ "engines": {
+ "node": ">= 4.8.0"
+ }
+ },
+ "node_modules/git-diff/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-diff/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-diff/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/git-diff/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/git-diff/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/git-diff/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-diff/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -14629,6 +14761,16 @@
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
},
+ "node_modules/interpret": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
+ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/io-ts": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz",
@@ -16941,6 +17083,120 @@
"node": ">=6"
}
},
+ "node_modules/kysely": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.2.tgz",
+ "integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/kysely-codegen": {
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/kysely-codegen/-/kysely-codegen-0.18.5.tgz",
+ "integrity": "sha512-bj6DMsXcKo0PrrXUk/fdjFgNC6Pwq+HPBCqhNGuD57gwUJZdci2s2OqhNneQeYpAIWGot7/481WdzTyXrClY2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "4.1.2",
+ "cosmiconfig": "^9.0.0",
+ "dotenv": "^16.5.0",
+ "dotenv-expand": "^12.0.2",
+ "git-diff": "^2.0.6",
+ "micromatch": "^4.0.8",
+ "minimist": "^1.2.8",
+ "pluralize": "^8.0.0",
+ "zod": "^3.24.4"
+ },
+ "bin": {
+ "kysely-codegen": "dist/cli/bin.js"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@libsql/kysely-libsql": ">=0.3.0 <0.5.0",
+ "@tediousjs/connection-string": ">=0.5.0 <0.6.0",
+ "better-sqlite3": ">=7.6.2 <8.0.0",
+ "kysely": ">=0.27.0 <1.0.0",
+ "kysely-bun-sqlite": ">=0.3.2 <1.0.0",
+ "kysely-bun-worker": ">=1.2.0 <2.0.0",
+ "mysql2": ">=2.3.3 <4.0.0",
+ "pg": ">=8.8.0 <9.0.0",
+ "tarn": ">=3.0.0 <4.0.0",
+ "tedious": ">=18.0.0 <20.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@libsql/kysely-libsql": {
+ "optional": true
+ },
+ "@tediousjs/connection-string": {
+ "optional": true
+ },
+ "better-sqlite3": {
+ "optional": true
+ },
+ "kysely": {
+ "optional": false
+ },
+ "kysely-bun-sqlite": {
+ "optional": true
+ },
+ "kysely-bun-worker": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "tarn": {
+ "optional": true
+ },
+ "tedious": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/kysely-codegen/node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/kysely-codegen/node_modules/pluralize": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/lamassu-admin": {
"resolved": "packages/admin-ui",
"link": true
@@ -21212,6 +21468,18 @@
"node": ">=8.10.0"
}
},
+ "node_modules/rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.1.6"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/referrer-policy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz",
@@ -22888,6 +23156,34 @@
"node": ">=8"
}
},
+ "node_modules/shelljs": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
+ "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "glob": "^7.0.0",
+ "interpret": "^1.0.0",
+ "rechoir": "^0.6.2"
+ },
+ "bin": {
+ "shjs": "bin/shjs"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/shelljs.exec": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/shelljs.exec/-/shelljs.exec-1.1.8.tgz",
+ "integrity": "sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/shellwords": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
@@ -25966,12 +26262,15 @@
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
+ "node_modules/typesafe-db": {
+ "resolved": "packages/typesafe-db",
+ "link": true
+ },
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -28778,9 +29077,7 @@
}
},
"packages/typesafe-db": {
- "name": "lamassu-typesafe-db",
"version": "11.0.0-beta.0",
- "extraneous": true,
"license": "../LICENSE",
"dependencies": {
"kysely": "^0.28.2",
diff --git a/package.json b/package.json
index 78242888..4808b5ea 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,8 @@
},
"workspaces": [
"packages/server",
- "packages/admin-ui"
+ "packages/admin-ui",
+ "packages/typesafe-db"
],
"devDependencies": {
"@eslint/css": "^0.7.0",
diff --git a/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx b/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx
index 0b7f1e2c..5a102a3b 100644
--- a/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx
+++ b/packages/admin-ui/src/pages/Customers/CustomerProfile.jsx
@@ -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,
diff --git a/packages/admin-ui/src/pages/Customers/CustomersList.jsx b/packages/admin-ui/src/pages/Customers/CustomersList.jsx
index 72530f04..9258e039 100644
--- a/packages/admin-ui/src/pages/Customers/CustomersList.jsx
+++ b/packages/admin-ui/src/pages/Customers/CustomersList.jsx
@@ -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 }) => ,
},
],
@@ -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 }) => (
+
+
+ onClick(row)}>
+
+
+
+
+ ),
})
return (
diff --git a/packages/admin-ui/src/pages/Customers/helper.test.js b/packages/admin-ui/src/pages/Customers/helper.test.js
index a3180eff..b8836d79 100644
--- a/packages/admin-ui/src/pages/Customers/helper.test.js
+++ b/packages/admin-ui/src/pages/Customers/helper.test.js
@@ -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',
+ })
+ })
})
diff --git a/packages/server/lib/customers.js b/packages/server/lib/customers.js
index 67e6d806..dec3e7d6 100644
--- a/packages/server/lib/customers.js
+++ b/packages/server/lib/customers.js
@@ -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 })
}
/**
diff --git a/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js b/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js
index 9dbf2951..c574b272 100644
--- a/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js
+++ b/packages/server/lib/new-admin/graphql/resolvers/customer.resolver.js
@@ -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),
},
diff --git a/packages/typesafe-db/package.json b/packages/typesafe-db/package.json
new file mode 100644
index 00000000..0423804b
--- /dev/null
+++ b/packages/typesafe-db/package.json
@@ -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"
+ }
+}
diff --git a/packages/typesafe-db/src/customers.ts b/packages/typesafe-db/src/customers.ts
new file mode 100644
index 00000000..d56f2790
--- /dev/null
+++ b/packages/typesafe-db/src/customers.ts
@@ -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
+
+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 {
+ 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('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('NOW', [])).as('isSuspended'),
+ fn('GREATEST', [
+ val(0),
+ fn('date_part', [
+ val('day'),
+ eb('c.suspendedUntil', '-', fn('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 }
diff --git a/packages/typesafe-db/src/db.ts b/packages/typesafe-db/src/db.ts
new file mode 100644
index 00000000..959d6161
--- /dev/null
+++ b/packages/typesafe-db/src/db.ts
@@ -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({
+ 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('---')
+ }
+ },
+})
diff --git a/packages/typesafe-db/src/types/types.d.ts b/packages/typesafe-db/src/types/types.d.ts
new file mode 100644
index 00000000..caaf0434
--- /dev/null
+++ b/packages/typesafe-db/src/types/types.d.ts
@@ -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 extends ColumnType
+ ? ColumnType
+ : ColumnType
+
+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
+
+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
+
+export type TradeType = 'buy' | 'sell'
+
+export type TransactionBatchStatus = 'failed' | 'open' | 'ready' | 'sent'
+
+export type VerificationType = 'automatic' | 'blocked' | 'verified'
+
+export interface AuthTokens {
+ expire: Generated
+ token: string
+ type: AuthTokenType
+ userId: string | null
+}
+
+export interface Bills {
+ cashboxBatchId: string | null
+ cashInFee: Numeric
+ cashInTxsId: string
+ created: Generated
+ cryptoCode: Generated
+ destinationUnit: Generated
+ deviceTime: Int8
+ fiat: number
+ fiatCode: string
+ id: string
+ legacy: Generated
+}
+
+export interface Blacklist {
+ address: string
+ blacklistMessageId: Generated
+}
+
+export interface BlacklistMessages {
+ allowToggle: Generated
+ content: string
+ id: string
+ label: string
+}
+
+export interface CashInActions {
+ action: string
+ created: Generated
+ error: string | null
+ errorCode: string | null
+ id: Generated
+ txHash: string | null
+ txId: string
+}
+
+export interface CashInTxs {
+ batched: Generated
+ batchId: string | null
+ batchTime: Timestamp | null
+ cashInFee: Numeric
+ commissionPercentage: Generated
+ created: Generated
+ cryptoAtoms: Numeric
+ cryptoCode: string
+ customerId: Generated
+ 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
+ minimumTx: number
+ operatorCompleted: Generated
+ phone: string | null
+ rawTickerPrice: Generated
+ send: Generated
+ sendConfirmed: Generated
+ sendPending: Generated
+ sendTime: Timestamp | null
+ termsAccepted: Generated
+ timedout: Generated
+ toAddress: string
+ txCustomerPhotoAt: Timestamp | null
+ txCustomerPhotoPath: string | null
+ txHash: string | null
+ txVersion: number
+ walletScore: number | null
+}
+
+export interface CashinTxTrades {
+ tradeId: Generated
+ txId: string
+}
+
+export interface CashOutActions {
+ action: string
+ created: Generated
+ 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
+ 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
+ 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
+ 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
+ confirmedAt: Timestamp | null
+ created: Generated
+ cryptoAtoms: Numeric
+ cryptoCode: string
+ customerId: Generated
+ 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
+ dispenseConfirmed: Generated
+ email: string | null
+ error: string | null
+ errorCode: string | null
+ fiat: Numeric
+ fiatCode: string
+ fixedFee: Generated
+ hdIndex: Generated
+ id: string
+ layer2Address: string | null
+ notified: Generated
+ 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
+ receivedCryptoAtoms: Generated
+ redeem: Generated
+ status: Generated
+ swept: Generated
+ termsAccepted: Generated
+ timedout: Generated
+ toAddress: string
+ txCustomerPhotoAt: Timestamp | null
+ txCustomerPhotoPath: string | null
+ txVersion: number
+ walletScore: number | null
+}
+
+export interface CashoutTxTrades {
+ tradeId: Generated
+ txId: string
+}
+
+export interface CashUnitOperation {
+ billCountOverride: number | null
+ created: Generated
+ 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
+}
+
+export interface CustomerCustomFieldPairs {
+ customerId: string
+ customFieldId: string
+ value: string
+}
+
+export interface CustomerExternalCompliance {
+ customerId: string
+ externalId: string
+ lastKnownStatus: ExternalComplianceStatus | null
+ lastUpdated: Generated
+ service: string
+}
+
+export interface CustomerNotes {
+ content: Generated
+ created: Generated
+ customerId: string
+ id: string
+ lastEditedAt: Timestamp | null
+ lastEditedBy: string | null
+ title: Generated
+}
+
+export interface Customers {
+ address: string | null
+ authorizedAt: Timestamp | null
+ authorizedOverride: Generated
+ authorizedOverrideAt: Timestamp | null
+ authorizedOverrideBy: string | null
+ created: Generated
+ email: string | null
+ emailAt: Timestamp | null
+ frontCameraAt: Timestamp | null
+ frontCameraOverride: Generated
+ 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
+ idCardDataOverrideAt: Timestamp | null
+ idCardDataOverrideBy: string | null
+ idCardDataRaw: string | null
+ idCardPhotoAt: Timestamp | null
+ idCardPhotoOverride: Generated
+ idCardPhotoOverrideAt: Timestamp | null
+ idCardPhotoOverrideBy: string | null
+ idCardPhotoPath: string | null
+ isTestCustomer: Generated
+ lastAuthAttempt: Timestamp | null
+ lastUsedMachine: string | null
+ name: string | null
+ phone: string | null
+ phoneAt: Timestamp | null
+ phoneOverride: Generated
+ phoneOverrideAt: Timestamp | null
+ phoneOverrideBy: string | null
+ sanctions: boolean | null
+ sanctionsAt: Timestamp | null
+ sanctionsOverride: Generated
+ sanctionsOverrideAt: Timestamp | null
+ sanctionsOverrideBy: string | null
+ smsOverride: Generated
+ 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
+ usSsnOverrideAt: Timestamp | null
+ usSsnOverrideBy: string | null
+}
+
+export interface CustomersCustomInfoRequests {
+ customerData: Json
+ customerId: string
+ infoRequestId: string
+ override: Generated
+ overrideAt: Timestamp | null
+ overrideBy: string | null
+}
+
+export interface CustomFieldDefinitions {
+ active: Generated
+ id: string
+ label: string
+}
+
+export interface CustomInfoRequests {
+ customRequest: Json | null
+ enabled: Generated
+ id: string
+}
+
+export interface Devices {
+ cassette1: Generated
+ cassette2: Generated
+ cassette3: Generated
+ cassette4: Generated
+ created: Generated
+ deviceId: string
+ diagnosticsFrontUpdatedAt: Timestamp | null
+ diagnosticsScanUpdatedAt: Timestamp | null
+ diagnosticsTimestamp: Timestamp | null
+ display: Generated
+ lastOnline: Generated
+ location: Generated
+ model: string | null
+ name: string
+ numberOfCassettes: Generated
+ numberOfRecyclers: Generated
+ paired: Generated
+ recycler1: Generated
+ recycler2: Generated
+ recycler3: Generated
+ recycler4: Generated
+ recycler5: Generated
+ recycler6: Generated
+ userConfigId: number | null
+ version: string | null
+}
+
+export interface EditedCustomerData {
+ created: Generated
+ 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
+ deviceId: string
+ fiat: number
+ fiatCode: string
+ id: string
+}
+
+export interface HardwareCredentials {
+ created: Generated
+ data: Json
+ id: string
+ lastUsed: Generated
+ userId: string
+}
+
+export interface IndividualDiscounts {
+ customerId: string
+ discount: number
+ id: string
+ softDeleted: Generated
+}
+
+export interface Logs {
+ deviceId: string | null
+ id: string
+ logLevel: string | null
+ message: string | null
+ serial: Generated
+ serverTimestamp: Generated
+ timestamp: Timestamp | null
+}
+
+export interface MachineEvents {
+ created: Generated
+ deviceId: string
+ deviceTime: Timestamp | null
+ eventType: string
+ id: string
+ note: string | null
+}
+
+export interface MachineNetworkHeartbeat {
+ averagePacketLoss: Numeric
+ averageResponseTime: Numeric
+ created: Generated
+ deviceId: string
+ id: string
+}
+
+export interface MachineNetworkPerformance {
+ created: Generated
+ deviceId: string
+ downloadSpeed: Numeric
+}
+
+export interface MachinePings {
+ deviceId: string
+ deviceTime: Timestamp
+ updated: Generated
+}
+
+export interface Migrations {
+ data: Json
+ id: Generated
+}
+
+export interface Notifications {
+ created: Generated
+ detail: Json | null
+ id: string
+ message: string
+ read: Generated
+ type: NotificationType
+ valid: Generated
+}
+
+export interface OperatorIds {
+ id: Generated
+ operatorId: string
+ service: string
+}
+
+export interface PairingTokens {
+ created: Generated
+ id: Generated
+ name: string
+ token: string | null
+}
+
+export interface SanctionsLogs {
+ created: Generated
+ 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
+}
+
+export interface SmsNotices {
+ allowToggle: Generated
+ created: Generated
+ enabled: Generated
+ event: SmsNoticeEvent
+ id: string
+ message: string
+ messageName: string
+}
+
+export interface Trades {
+ created: Generated
+ cryptoAtoms: Numeric
+ cryptoCode: string
+ error: string | null
+ fiatCode: string
+ id: Generated
+ type: TradeType
+}
+
+export interface TransactionBatches {
+ closedAt: Timestamp | null
+ createdAt: Generated
+ cryptoCode: string
+ errorMessage: string | null
+ id: string
+ status: Generated
+}
+
+export interface UnpairedDevices {
+ deviceId: string
+ id: string
+ model: string | null
+ name: string | null
+ paired: Timestamp
+ unpaired: Timestamp
+}
+
+export interface UserConfig {
+ created: Generated
+ data: Json
+ id: Generated
+ schemaVersion: Generated
+ type: string
+ valid: boolean
+}
+
+export interface UserRegisterTokens {
+ expire: Generated
+ role: Generated
+ token: string
+ useFido: Generated
+ username: string
+}
+
+export interface Users {
+ created: Generated
+ enabled: Generated
+ id: string
+ lastAccessed: Generated
+ lastAccessedAddress: string | null
+ lastAccessedFrom: string | null
+ password: string | null
+ role: Generated
+ 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
+}
diff --git a/packages/typesafe-db/tsconfig.json b/packages/typesafe-db/tsconfig.json
new file mode 100644
index 00000000..aa077c9f
--- /dev/null
+++ b/packages/typesafe-db/tsconfig.json
@@ -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"]
+}