Merge branch 'dev' into feat/lam-1291/stress-testing
* dev: (41 commits) build: use bullseye as target build chore: remove whitespace refactor: simplify denominations list construction fix: sort coins by descending denomination feat: address prompt feature toggle on ui feat: reuse last address option fix: performance issues on SystemPerformance chore: clarify requirements on comment feat: allow address reuse if same customer feat: address reuse is now per customer fix: hide anon and show phone on customers fix: dev environment restarts feat: batch diagnostics script fix: custom info request returns array fix: name on customer if custom data is filled build: testing cache hit build: server cache improvements build: node_modules was ignored on .dockerignored build: leftovers from npm chore: commented by mistake ...
This commit is contained in:
commit
5feee6d5df
105 changed files with 17323 additions and 31348 deletions
|
|
@ -1,4 +1,5 @@
|
|||
**/node_modules
|
||||
node_modules
|
||||
packages/*/node_modules
|
||||
.git
|
||||
.direnv
|
||||
.envrc
|
||||
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
|
|
@ -4,42 +4,48 @@ on: [ workflow_dispatch ]
|
|||
|
||||
jobs:
|
||||
everything:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-updatetar
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-updatetar
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: build/ci.Dockerfile
|
||||
load: true
|
||||
tags: ci_image:latest
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Extract artifact from Docker image
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.11.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages with Turbo
|
||||
run: pnpm run build
|
||||
|
||||
- name: Package production build
|
||||
run: |
|
||||
docker create --name extract_artifact ci_image:latest
|
||||
docker cp extract_artifact:/lamassu-server.tar.gz ./lamassu-server.tar.gz
|
||||
docker rm extract_artifact
|
||||
# Create production-ready server package using pnpm deploy
|
||||
pnpm deploy --filter=./packages/server --prod lamassu-server --legacy
|
||||
|
||||
# Copy built admin UI to public directory
|
||||
cp -r packages/admin-ui/build lamassu-server/public
|
||||
|
||||
# Create tarball
|
||||
tar -zcf lamassu-server.tar.gz lamassu-server/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lamassu-server.tar.gz
|
||||
path: lamassu-server.tar.gz
|
||||
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
76
.github/workflows/docker-build.yml
vendored
76
.github/workflows/docker-build.yml
vendored
|
|
@ -11,22 +11,57 @@ env:
|
|||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-updatetar
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-updatetar
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.11.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages with Turbo
|
||||
run: pnpm run build
|
||||
|
||||
- name: Verify build artifacts
|
||||
run: |
|
||||
echo "=== Verifying typesafe-db build ==="
|
||||
ls -la packages/typesafe-db/lib/
|
||||
echo "=== Verifying admin-ui build ==="
|
||||
ls -la packages/admin-ui/build/
|
||||
|
||||
- name: Package production build
|
||||
run: |
|
||||
# Create production-ready server package using pnpm deploy
|
||||
pnpm deploy --filter=./packages/server --prod lamassu-server --legacy
|
||||
|
||||
# Copy built admin UI to public directory
|
||||
cp -r packages/admin-ui/build lamassu-server/public
|
||||
|
||||
# Copy Dockerfile to lamassu-server context
|
||||
cp build/server.Dockerfile lamassu-server/
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
|
|
@ -34,29 +69,24 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push server image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
context: lamassu-server
|
||||
push: true
|
||||
target: l-s
|
||||
file: ./build/server.Dockerfile
|
||||
file: lamassu-server/server.Dockerfile
|
||||
tags: ${{ env.DOCKERHUB_SERVER_REPO }}:latest
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push admin server image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
context: lamassu-server
|
||||
push: true
|
||||
target: l-a-s
|
||||
file: ./build/server.Dockerfile
|
||||
file: lamassu-server/server.Dockerfile
|
||||
tags: ${{ env.DOCKERHUB_ADMIN_REPO }}:latest
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
|
|||
40
.github/workflows/test.yml
vendored
Normal file
40
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ dev ]
|
||||
push:
|
||||
branches: [ dev ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.11.0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run test
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,9 +1,13 @@
|
|||
**/node_modules
|
||||
**/.env
|
||||
.pnpm-store/
|
||||
|
||||
.idea/
|
||||
.settings/
|
||||
.turbo/
|
||||
|
||||
packages/server/.lamassu
|
||||
packages/server/certs/
|
||||
packages/server/tests/stress/machines
|
||||
packages/server/tests/stress/config.json
|
||||
packages/typesafe-db/lib/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Run linting
|
||||
npx lint-staged
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
nodejs 22
|
||||
pnpm 10
|
||||
python 3
|
||||
|
|
|
|||
115
INSTALL-NIX.md
115
INSTALL-NIX.md
|
|
@ -1,115 +0,0 @@
|
|||
# Install - nix
|
||||
|
||||
## Preliminaries for using nix
|
||||
|
||||
For a dev environment with nix package manager a postgres install on the base system is required, this guide does not cover a postgresql server running with nix-shell.
|
||||
|
||||
### Set up PostgreSQL
|
||||
|
||||
```
|
||||
sudo -u postgres createdb lamassu
|
||||
sudo -u postgres psql postgres
|
||||
```
|
||||
|
||||
In `psql`, run the following and set password to `postgres123`:
|
||||
|
||||
```
|
||||
\password postgres
|
||||
ctrl-d
|
||||
```
|
||||
|
||||
### Starting up environment
|
||||
|
||||
shell.nix script provided, all you need to do to setup the environment is to run `nix-shell` on the folder.
|
||||
|
||||
## Installation
|
||||
|
||||
### Install node modules
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Generate certificates
|
||||
|
||||
```
|
||||
bash packages/server/tools/cert-gen.sh
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- This will create a `.lamassu` directory in your home directory.
|
||||
|
||||
### Set up database
|
||||
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway.
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-migrate
|
||||
```
|
||||
|
||||
### Run new-lamassu-admin
|
||||
|
||||
```
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
|
||||
### Run lamassu-admin-server
|
||||
|
||||
In a second terminal window:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
### Register admin user
|
||||
|
||||
In a third terminal window:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-register admin@example.com superuser
|
||||
```
|
||||
|
||||
You'll use this generated URL in the brower in a moment.
|
||||
|
||||
### Complete configuration
|
||||
|
||||
Paste the URL from lamassu-register exactly as output, into a browser (chrome or firefox).
|
||||
|
||||
**Important**: the host must be localhost. Tell your browser to trust the certificate even though it's not signed by a recognized CA. If you get an "expired" error, try opening https://localhost:8070/graphql in another tab and trust the certificate.
|
||||
|
||||
Go to all the required, unconfigured red fields and choose some values. Choose mock services whenever available.
|
||||
|
||||
### Run lamassu-server
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
### Add a lamassu-machine
|
||||
|
||||
Click on `+ Add Machine` in the sidebar. Type in a name for your machine and click **Pair**. Open up development tools to show the JavaScript console and copy the totem. You will use this to run lamassu-machine. This pairing totem expires after an hour.
|
||||
|
||||
Now continue with lamassu-machine instructions from the `INSTALL.md` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine).
|
||||
|
||||
## Subsequent runs
|
||||
|
||||
To start the Lamassu server run:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
To start the Lamassu Admin run:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
126
INSTALL.md
126
INSTALL.md
|
|
@ -1,126 +0,0 @@
|
|||
# Install
|
||||
|
||||
## Preliminaries for Ubuntu 16.04
|
||||
|
||||
Installation for other distros may be slightly different. This assumes NodeJS 22 or higher is already installed. All of this is done in the lamassu-server directory.
|
||||
|
||||
### Packages
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install postgresql postgresql-contrib postgresql-server-dev-9.5 libpq-dev git
|
||||
```
|
||||
|
||||
### Set up PostgreSQL
|
||||
|
||||
```
|
||||
sudo -u postgres createdb lamassu
|
||||
sudo -u postgres psql postgres
|
||||
```
|
||||
|
||||
In `psql`, run the following and set password to `postgres123`:
|
||||
|
||||
```
|
||||
\password postgres
|
||||
ctrl-d
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Install node modules
|
||||
|
||||
Make sure you're running NodeJS 22 or higher. Ignore any warnings.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Generate certificates
|
||||
|
||||
```
|
||||
bash packages/server/tools/cert-gen.sh
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- This will create a `.lamassu` directory in your home directory.
|
||||
|
||||
### Set up database
|
||||
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway.
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-migrate
|
||||
```
|
||||
|
||||
### Run new-lamassu-admin
|
||||
|
||||
```
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
|
||||
### Run lamassu-admin-server
|
||||
|
||||
In a second terminal window:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
### Register admin user
|
||||
|
||||
In a third terminal window:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-register admin@example.com superuser
|
||||
```
|
||||
|
||||
You'll use this generated URL in the brower in a moment.
|
||||
|
||||
### Complete configuration
|
||||
|
||||
Paste the URL from lamassu-register exactly as output, into a browser (chrome or firefox).
|
||||
|
||||
**Important**: the host must be localhost. Tell your browser to trust the certificate even though it's not signed by a recognized CA. If you get an "expired" error, try opening https://localhost:8070/graphql in another tab and trust the certificate.
|
||||
|
||||
Go to all the required, unconfigured red fields and choose some values. Choose mock services whenever available.
|
||||
|
||||
### Run lamassu-server
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
### Add a lamassu-machine
|
||||
|
||||
Click on `+ Add Machine` in the sidebar. Type in a name for your machine and click **Pair**. Open up development tools to show the JavaScript console and copy the totem. You will use this to run lamassu-machine. This pairing totem expires after an hour.
|
||||
|
||||
Now continue with lamassu-machine instructions from the `INSTALL.md` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine).
|
||||
|
||||
### Run a local coin node (BTC supported)
|
||||
|
||||
Run `node packages/server/bin/lamassu-coins` in the project root and select `Bitcoin`. This process will require the existence of certain environment variables that the setup will warn about.
|
||||
|
||||
Once that is done, the node needs to be run in a terminal with the following command `<YOUR_BLOCKCHAIN_DIR_ENV_VAR>/bin/bitcoind -datadir=<YOUR_BLOCKCHAIN_DIR_ENV_VAR>/bitcoin`
|
||||
|
||||
## Subsequent runs
|
||||
|
||||
To start the Lamassu server run:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-server --mockScoring
|
||||
```
|
||||
|
||||
To start the Lamassu Admin run:
|
||||
|
||||
```
|
||||
node packages/server/bin/lamassu-admin-server --dev
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```
|
||||
cd packages/admin-ui/
|
||||
npm run start
|
||||
```
|
||||
55
README.md
55
README.md
|
|
@ -6,14 +6,57 @@ Lamassu remote server.
|
|||
|
||||
We do not generally accept outside pull requests for new features. Please consult with us before putting a lot of work into a pull request.
|
||||
|
||||
## Installation
|
||||
## Development
|
||||
|
||||
See [INSTALL.md](INSTALL.md), or [INSTALL-NIX.md](INSTALL-NIX.md) for nix environment.
|
||||
### Requirements
|
||||
|
||||
## Installation on remote server (only for production)
|
||||
See [lamassu-remote-install/README.md](lamassu-remote-install/README.md).
|
||||
- Nodejs 22
|
||||
- PNPM 10+
|
||||
- Postgres Database
|
||||
- Python 3 (to be deprecated, required by a single dependency installation)
|
||||
- OpenSSL (for cert-gen.sh, it will set up the server self-signed certificates)
|
||||
|
||||
There's a shell.nix file that you can use to set up your env in case you're a nix user. (most reliable way of installing native deps)
|
||||
There's also a .tool-versions for asdf and mise users.
|
||||
|
||||
This project uses Turbo for monorepo management. Install dependencies:
|
||||
|
||||
## Running
|
||||
```bash
|
||||
node bin/lamassu-server --mockScoring
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Prepare environment files:
|
||||
|
||||
```bash
|
||||
bash packages/server/tools/cert-gen.sh
|
||||
```
|
||||
|
||||
On packages/server/.env you can alter variables such as the postgres connection info.
|
||||
|
||||
After configuring the postgres connection, run:
|
||||
|
||||
```bash
|
||||
node packages/server/bin/lamassu-migrate
|
||||
```
|
||||
|
||||
### Start development environment:
|
||||
|
||||
If you've already done the setup, you can run:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Creating a user
|
||||
|
||||
```bash
|
||||
node packages/server/bin/lamassu-register admin@example.com superuser
|
||||
```
|
||||
|
||||
### Pairing a machine
|
||||
|
||||
To get the pairing token from the QRCode open the browser console before picking the name of the machine, the token should appear on the terminal.
|
||||
It's also possible to inspect the qrCode, the token is on the data-cy="" attr.
|
||||
Lastly, you can always scan it with a phone and copy the contents over.
|
||||
|
||||
Now continue with lamassu-machine instructions from the `INSTALL.md` file in [lamassu-machine repository](https://github.com/lamassu/lamassu-machine)
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
FROM node:22-alpine AS build-ui
|
||||
RUN apk add --no-cache npm git curl build-base python3
|
||||
|
||||
COPY ["packages/admin-ui/package.json", "package-lock.json", "./"]
|
||||
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install
|
||||
|
||||
COPY packages/admin-ui/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM ubuntu:20.04 as base
|
||||
|
||||
ARG VERSION
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Europe/Lisbon
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
RUN apt-get install -y -q curl \
|
||||
sudo \
|
||||
git \
|
||||
python2-minimal \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
net-tools \
|
||||
tar
|
||||
|
||||
RUN curl -sL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
RUN apt-get install nodejs -y -q
|
||||
|
||||
WORKDIR lamassu-server
|
||||
|
||||
COPY ["packages/server/package.json", "package-lock.json", "./"]
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install --production
|
||||
|
||||
COPY ./packages/server/ ./
|
||||
COPY --from=build-ui /build /lamassu-server/public
|
||||
|
||||
RUN cd .. && tar -zcvf lamassu-server.tar.gz ./lamassu-server
|
||||
|
|
@ -2,20 +2,16 @@ version: "3.8"
|
|||
|
||||
services:
|
||||
lamassu-server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: build/server.Dockerfile
|
||||
target: l-s
|
||||
image: lamassu/lamassu-server:latest
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 3000:3000
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./lamassu-data:/lamassu-data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres123
|
||||
- POSTGRES_HOST=host.docker.internal
|
||||
- POSTGRES_HOST=localhost
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=lamassu
|
||||
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
||||
|
|
@ -31,20 +27,16 @@ services:
|
|||
- LOG_LEVEL=info
|
||||
|
||||
lamassu-admin-server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: build/server.Dockerfile
|
||||
target: l-a-s
|
||||
image: lamassu/lamassu-admin-server:latest
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 443:443
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./lamassu-data:/lamassu-data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres123
|
||||
- POSTGRES_HOST=host.docker.internal
|
||||
- POSTGRES_HOST=localhost
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=lamassu
|
||||
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
||||
|
|
@ -1,49 +1,17 @@
|
|||
FROM node:22-alpine AS build
|
||||
RUN apk add --no-cache npm git curl build-base net-tools python3 postgresql-dev
|
||||
FROM node:22-bullseye AS base
|
||||
RUN apt install openssl ca-certificates
|
||||
|
||||
WORKDIR /lamassu-server
|
||||
|
||||
COPY ["packages/server/package.json", "package-lock.json", "./"]
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install --production
|
||||
|
||||
COPY packages/server/ ./
|
||||
|
||||
|
||||
FROM node:22-alpine AS l-s-base
|
||||
RUN apk add --no-cache npm git curl bash libpq openssl ca-certificates
|
||||
|
||||
COPY --from=build /lamassu-server /lamassu-server
|
||||
|
||||
|
||||
FROM l-s-base AS l-s
|
||||
# Copy the pre-built production package from CI (with node_modules)
|
||||
COPY . ./
|
||||
|
||||
FROM base AS l-s
|
||||
RUN chmod +x /lamassu-server/bin/lamassu-server-entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ]
|
||||
|
||||
|
||||
FROM node:22-alpine AS build-ui
|
||||
RUN apk add --no-cache npm git curl build-base python3
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ["packages/admin-ui/package.json", "package-lock.json", "./"]
|
||||
|
||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
||||
RUN npm install
|
||||
|
||||
COPY packages/admin-ui/ ./
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM l-s-base AS l-a-s
|
||||
COPY --from=build-ui /app/build /lamassu-server/public
|
||||
|
||||
FROM base AS l-a-s
|
||||
RUN chmod +x /lamassu-server/bin/lamassu-admin-server-entrypoint.sh
|
||||
|
||||
EXPOSE 443
|
||||
|
||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-admin-server-entrypoint.sh" ]
|
||||
|
|
@ -5,10 +5,11 @@ import json from '@eslint/json'
|
|||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
||||
import pluginJest from 'eslint-plugin-jest'
|
||||
import vitest from 'eslint-plugin-vitest'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
'**/.lamassu',
|
||||
'**/build',
|
||||
'**/package.json',
|
||||
'**/package-lock.json',
|
||||
|
|
@ -59,16 +60,12 @@ export default defineConfig([
|
|||
{
|
||||
// update this to match your test files
|
||||
files: ['**/*.spec.js', '**/*.test.js'],
|
||||
plugins: { jest: pluginJest },
|
||||
languageOptions: {
|
||||
globals: pluginJest.environments.globals.globals,
|
||||
plugins: {
|
||||
vitest,
|
||||
},
|
||||
rules: {
|
||||
'jest/no-disabled-tests': 'warn',
|
||||
'jest/no-focused-tests': 'error',
|
||||
'jest/no-identical-title': 'error',
|
||||
'jest/prefer-to-have-length': 'warn',
|
||||
'jest/valid-expect': 'error',
|
||||
...vitest.configs.recommended.rules, // you can also use vitest.configs.all.rules to enable all rules
|
||||
'vitest/max-nested-describe': ['error', { max: 3 }], // you can also modify rules' behavior using option like this
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
|||
30375
package-lock.json
generated
30375
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
|
@ -4,30 +4,34 @@
|
|||
"version": "11.0.0-beta.1",
|
||||
"license": "./LICENSE",
|
||||
"author": "Lamassu (https://lamassu.is)",
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lamassu/lamassu-server.git"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/server",
|
||||
"packages/admin-ui"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/css": "^0.7.0",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/json": "^0.12.0",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"eslint-plugin-vitest": "^0.5.4",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"prettier": "^3.5.3"
|
||||
"prettier": "^3.5.3",
|
||||
"turbo": "^2.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky install"
|
||||
"prepare": "husky install",
|
||||
"build": "turbo build",
|
||||
"dev": "turbo dev",
|
||||
"test": "turbo test"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
|
|
|
|||
|
|
@ -10,31 +10,27 @@ To take advantage of that make sure to run `git commit` from within this folder.
|
|||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
From the root directory (recommended with Turbo):
|
||||
|
||||
### `npm start`
|
||||
- `pnpm run dev` - Start development environment
|
||||
- `pnpm run build` - Build for production
|
||||
- `pnpm run admin:dev` - Start only admin UI development
|
||||
|
||||
Runs the app in the development mode.<br>
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
In the admin-ui package directory, you can run:
|
||||
|
||||
The page will reload if you make edits.<br>
|
||||
### `pnpm start` or `pnpm run dev`
|
||||
|
||||
Runs the app in development mode with Vite.
|
||||
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm fix`
|
||||
### `pnpm test`
|
||||
|
||||
Runs eslint --fix on the src folder
|
||||
Launches the test runner with vitest.
|
||||
|
||||
### `npm test`
|
||||
### `pnpm run build`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br>
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br>
|
||||
Builds the app for production to the `build` folder.
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br>
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"@lamassu/coins": "v1.6.1",
|
||||
"@mui/icons-material": "^7.1.0",
|
||||
"@mui/material": "^7.1.0",
|
||||
"@mui/x-date-pickers": "^8.3.1",
|
||||
"@simplewebauthn/browser": "^3.0.0",
|
||||
"apollo-upload-client": "^18.0.0",
|
||||
"bignumber.js": "9.0.0",
|
||||
|
|
@ -25,9 +26,10 @@
|
|||
"jszip": "^3.6.0",
|
||||
"libphonenumber-js": "^1.11.15",
|
||||
"match-sorter": "^4.2.0",
|
||||
"material-react-table": "^3.2.1",
|
||||
"pretty-ms": "^2.1.0",
|
||||
"qrcode.react": "4.2.0",
|
||||
"ramda": "^0.26.1",
|
||||
"ramda": "^0.30.1",
|
||||
"react": "18.3.1",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "18.3.1",
|
||||
|
|
@ -49,12 +51,15 @@
|
|||
"prettier": "3.4.1",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vitest": "^3.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'
|
|||
import React, { useState } from 'react'
|
||||
import { Router } from 'wouter'
|
||||
import ApolloProvider from './utils/apollo'
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers'
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV2'
|
||||
|
||||
import AppContext from './AppContext'
|
||||
import theme from './styling/theme'
|
||||
|
|
@ -33,16 +35,18 @@ const App = () => {
|
|||
isDirtyForm,
|
||||
setDirtyForm,
|
||||
}}>
|
||||
<Router hook={useLocationWithConfirmation}>
|
||||
<ApolloProvider>
|
||||
<StyledEngineProvider enableCssLayer>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Main />
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</ApolloProvider>
|
||||
</Router>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<Router hook={useLocationWithConfirmation}>
|
||||
<ApolloProvider>
|
||||
<StyledEngineProvider enableCssLayer>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Main />
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</ApolloProvider>
|
||||
</Router>
|
||||
</LocalizationProvider>
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import Paper from '@mui/material/Paper'
|
||||
|
|
@ -18,9 +17,5 @@ const CollapsibleCard = ({ className, state, shrunkComponent, children }) => {
|
|||
)
|
||||
}
|
||||
|
||||
CollapsibleCard.propTypes = {
|
||||
shrunkComponent: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
export default CollapsibleCard
|
||||
export { cardState }
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const NotificationCenter = ({
|
|||
const notificationsToShow =
|
||||
!showingUnread || !hasUnread
|
||||
? notifications
|
||||
: R.filter(R.propEq('read', false))(notifications)
|
||||
: R.filter(R.propEq(false, 'read'))(notifications)
|
||||
return notificationsToShow.map(n => {
|
||||
return (
|
||||
<NotificationRow
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
|||
}
|
||||
|
||||
const groupStriped = elements => {
|
||||
const [toStripe, noStripe] = R.partition(R.propEq('stripe', true))(elements)
|
||||
const [toStripe, noStripe] = R.partition(R.propEq(true, 'stripe'))(elements)
|
||||
|
||||
if (!toStripe.length) {
|
||||
return elements
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const ETable = ({
|
|||
setSaving(true)
|
||||
|
||||
const it = validationSchema.cast(value, { assert: 'ignore-optionality' })
|
||||
const index = R.findIndex(R.propEq('id', it.id))(data)
|
||||
const index = R.findIndex(R.propEq(it.id, 'id'))(data)
|
||||
const list = index !== -1 ? R.update(index, it, data) : R.prepend(it, data)
|
||||
|
||||
if (!R.equals(data[index], it)) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const Autocomplete = ({
|
|||
autoFocus,
|
||||
...props
|
||||
}) => {
|
||||
const mapFromValue = options => it => R.find(R.propEq(valueProp, it))(options)
|
||||
const mapFromValue = options => it => R.find(R.propEq(it, valueProp))(options)
|
||||
const mapToValue = R.prop(valueProp)
|
||||
|
||||
const getValue = () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const ToggleButtonGroup = ({
|
|||
}) => {
|
||||
return (
|
||||
<MUIToggleButtonGroup
|
||||
className="flex flex-col gap-4"
|
||||
size={size}
|
||||
name={name}
|
||||
orientation={orientation}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@
|
|||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/*TODO important because of tailwind integration with MUI*/
|
||||
.fullPartP {
|
||||
color: white;
|
||||
color: white !important;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useLazyQuery, useQuery, gql } from '@apollo/client'
|
||||
import { subMinutes } from 'date-fns'
|
||||
import FileSaver from 'file-saver'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import Modal from '../Modal'
|
||||
import { H3, P } from '../typography'
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ const createCsv = async ({ machineLogsCsv }) => {
|
|||
const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
||||
const [state, setState] = useState(STATES.INITIAL)
|
||||
const [timestamp, setTimestamp] = useState(null)
|
||||
let timeout = null
|
||||
const timeoutRef = useRef(null)
|
||||
|
||||
const [fetchSummary, { loading }] = useLazyQuery(MACHINE_LOGS, {
|
||||
onCompleted: data => createCsv(data),
|
||||
|
|
@ -76,24 +76,41 @@ const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
|||
data.machine.diagnostics.timestamp &&
|
||||
data.machine.diagnostics.timestamp !== timestamp
|
||||
) {
|
||||
clearTimeout(timeout)
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
timeoutRef.current = null
|
||||
}
|
||||
setTimestamp(data.machine.diagnostics.timestamp)
|
||||
setState(STATES.FILLED)
|
||||
stopPolling()
|
||||
}
|
||||
}, [data, stopPolling, timeout, timestamp])
|
||||
}, [data, stopPolling, timestamp])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
timeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const path = `/operator-data/diagnostics/${deviceId}/`
|
||||
|
||||
function runDiagnostics() {
|
||||
const runDiagnostics = () => {
|
||||
setState(STATES.RUNNING)
|
||||
startPolling(2000)
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setState(STATES.FAILURE)
|
||||
stopPolling()
|
||||
timeoutRef.current = null
|
||||
}, 60 * 1000)
|
||||
|
||||
setState(STATES.RUNNING)
|
||||
sendAction()
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +157,7 @@ const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
|||
<H3>Scan</H3>
|
||||
<img
|
||||
className="w-88"
|
||||
src={path + 'scan.jpg'}
|
||||
src={`${path}scan.jpg?${Date.now()}`}
|
||||
alt="Failure getting photo"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -148,7 +165,7 @@ const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
|||
<H3>Front</H3>
|
||||
<img
|
||||
className="w-88"
|
||||
src={path + 'front.jpg'}
|
||||
src={`${path}front.jpg?${Date.now()}`}
|
||||
alt="Failure getting photo"
|
||||
/>
|
||||
<P></P>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import React, { memo } from 'react'
|
|||
const TableRow = memo(
|
||||
({ className, children, header, error, success, size = 'sm', ...props }) => {
|
||||
const classnamesObj = {
|
||||
'p-1 h-12 bg-white': !header,
|
||||
'p-1 bg-white': !header,
|
||||
'h-12': !header && size !== 'sm' && size !== 'lg',
|
||||
'h-8': !header && size === 'sm',
|
||||
'h-9 font-bold text-base ': !header && size === 'lg',
|
||||
'bg-misty-rose': error,
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ const Analytics = () => {
|
|||
|
||||
const convertFiatToLocale = item => {
|
||||
if (item.fiatCode === fiatLocale) return item
|
||||
const itemRate = R.find(R.propEq('code', item.fiatCode))(rates)
|
||||
const itemRate = R.find(R.propEq(item.fiatCode, 'code'))(rates)
|
||||
const localeRate = R.find(R.propEq('code', fiatLocale))(rates)
|
||||
const multiplier = localeRate?.rate / itemRate?.rate
|
||||
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
|||
{R.isEmpty(machines) && <EmptyTable message="No machines so far" />}
|
||||
{wizard && (
|
||||
<Wizard
|
||||
machine={R.find(R.propEq('deviceId', wizard))(machines)}
|
||||
machine={R.find(R.propEq(wizard, 'deviceId'))(machines)}
|
||||
onClose={() => setWizard(false)}
|
||||
save={save}
|
||||
error={error?.message}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const Wizard = ({ machine, locale, onClose, save, error }) => {
|
|||
)
|
||||
}
|
||||
|
||||
const newConfig = R.merge(config, it)
|
||||
const newConfig = R.mergeRight(config, it)
|
||||
|
||||
setState({
|
||||
step: step + 1,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const CommissionsDetails = memo(
|
|||
initialValues={commission}
|
||||
save={save}
|
||||
validationSchema={getSchema(locale)}
|
||||
data={R.of(commission)}
|
||||
data={R.of(Array, commission)}
|
||||
elements={mainFields(currency)}
|
||||
setEditing={onEditingDefault}
|
||||
forceDisable={isEditingOverrides}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const getElement = (code, display) => ({
|
|||
const sortCommissionsBy = prop => {
|
||||
switch (prop) {
|
||||
case ORDER_OPTIONS[0]:
|
||||
return R.sortBy(R.find(R.propEq('code', R.prop('machine'))))
|
||||
return R.sortBy(R.find(R.propEq(R.prop('machine'), 'code')))
|
||||
case ORDER_OPTIONS[1]:
|
||||
return R.sortBy(R.path(['cryptoCurrencies', 0]))
|
||||
default:
|
||||
|
|
@ -80,7 +80,7 @@ const CommissionsList = memo(
|
|||
|
||||
const getMachineCoins = deviceId => {
|
||||
const override = R.prop('overrides', localeConfig)?.find(
|
||||
R.propEq('machine', deviceId),
|
||||
R.propEq(deviceId, 'machine'),
|
||||
)
|
||||
|
||||
const machineCoins = override
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const getView = (data, code, compare) => it => {
|
|||
if (!data) return ''
|
||||
|
||||
// The following boolean should come undefined if it is rendering an unpaired machine
|
||||
const attribute = R.find(R.propEq(compare ?? 'code', it))(data)
|
||||
const attribute = R.find(R.propEq(it, compare ?? 'code'))(data)
|
||||
|
||||
return attribute ? R.prop(code, attribute) : 'Unpaired machine'
|
||||
}
|
||||
|
|
@ -294,8 +294,8 @@ const getAlreadyUsed = (id, machine, values) => {
|
|||
const getCrypto = R.prop('cryptoCurrencies')
|
||||
const getMachineId = R.prop('machine')
|
||||
|
||||
const filteredOverrides = R.filter(R.propEq('machine', machine))(values)
|
||||
const originalValue = R.find(R.propEq('id', id))(values)
|
||||
const filteredOverrides = R.filter(R.propEq(machine, 'machine'))(values)
|
||||
const originalValue = R.find(R.propEq(id, 'id'))(values)
|
||||
|
||||
const originalCryptos = getCrypto(originalValue)
|
||||
const originalMachineId = getMachineId(originalValue)
|
||||
|
|
@ -407,7 +407,7 @@ const overridesDefaults = {
|
|||
|
||||
const getOrder = ({ machine, cryptoCurrencies }) => {
|
||||
const isAllMachines = machine === ALL_MACHINES.deviceId
|
||||
const isAllCoins = R.contains(ALL_COINS.code, cryptoCurrencies)
|
||||
const isAllCoins = R.includes(ALL_COINS.code, cryptoCurrencies)
|
||||
|
||||
if (isAllMachines && isAllCoins) return 0
|
||||
if (isAllMachines) return 1
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ const CustomerData = ({
|
|||
deleteEditedData: () => deleteEditedData({ idCardData: null }),
|
||||
save: values =>
|
||||
editCustomer({
|
||||
idCardData: R.merge(idData, formatDates(values)),
|
||||
idCardData: R.mergeRight(idData, formatDates(values)),
|
||||
}),
|
||||
validationSchema: customerDataSchemas.idCardData,
|
||||
checkAgainstSanctions: () =>
|
||||
|
|
@ -167,7 +167,7 @@ const CustomerData = ({
|
|||
save: values => {
|
||||
editCustomer({
|
||||
subscriberInfo: {
|
||||
result: R.merge(smsData, R.omit(['phoneNumber'])(values)),
|
||||
result: R.mergeRight(smsData, R.omit(['phoneNumber'])(values)),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { useQuery, useMutation, gql } from '@apollo/client'
|
|||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { useLocation } from 'wouter'
|
||||
import SearchBox from '../../components/SearchBox'
|
||||
import SearchFilter from '../../components/SearchFilter'
|
||||
import TitleSection from '../../components/layout/TitleSection'
|
||||
import TxInIcon from '../../styling/icons/direction/cash-in.svg?react'
|
||||
import TxOutIcon from '../../styling/icons/direction/cash-out.svg?react'
|
||||
|
|
@ -15,15 +13,6 @@ import CustomersList from './CustomersList'
|
|||
import CreateCustomerModal from './components/CreateCustomerModal'
|
||||
import { getAuthorizedStatus } from './helper'
|
||||
|
||||
const GET_CUSTOMER_FILTERS = gql`
|
||||
query filters {
|
||||
customerFilters {
|
||||
type
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const GET_CUSTOMERS = gql`
|
||||
query configAndCustomers(
|
||||
$phone: String
|
||||
|
|
@ -91,38 +80,27 @@ const CREATE_CUSTOMER = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const getFiltersObj = filters =>
|
||||
R.reduce((s, f) => ({ ...s, [f.type]: f.value }), {}, filters)
|
||||
|
||||
const Customers = () => {
|
||||
const [, navigate] = useLocation()
|
||||
|
||||
const handleCustomerClicked = customer =>
|
||||
navigate(`/compliance/customer/${customer.id}`)
|
||||
|
||||
const [filteredCustomers, setFilteredCustomers] = useState([])
|
||||
const [variables, setVariables] = useState({})
|
||||
const [filters, setFilters] = useState([])
|
||||
const [customers, setCustomers] = useState([])
|
||||
const [showCreationModal, setShowCreationModal] = useState(false)
|
||||
|
||||
const {
|
||||
data: customersResponse,
|
||||
loading: customerLoading,
|
||||
refetch,
|
||||
} = useQuery(GET_CUSTOMERS, {
|
||||
variables,
|
||||
onCompleted: data => setFilteredCustomers(R.path(['customers'])(data)),
|
||||
})
|
||||
|
||||
const { data: filtersResponse, loading: loadingFilters } =
|
||||
useQuery(GET_CUSTOMER_FILTERS)
|
||||
const { data: customersResponse, loading: customerLoading } = useQuery(
|
||||
GET_CUSTOMERS,
|
||||
{
|
||||
onCompleted: data => setCustomers(R.path(['customers'])(data)),
|
||||
},
|
||||
)
|
||||
|
||||
const [createNewCustomer] = useMutation(CREATE_CUSTOMER, {
|
||||
onCompleted: () => setShowCreationModal(false),
|
||||
refetchQueries: () => [
|
||||
{
|
||||
query: GET_CUSTOMERS,
|
||||
variables,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
@ -145,76 +123,12 @@ const Customers = () => {
|
|||
const customersData = R.pipe(
|
||||
R.map(setAuthorizedStatus),
|
||||
R.sortWith([R.ascend(byAuthorized), R.descend(byLastActive)]),
|
||||
)(filteredCustomers ?? [])
|
||||
|
||||
const onFilterChange = filters => {
|
||||
const filtersObject = getFiltersObj(filters)
|
||||
|
||||
setFilters(filters)
|
||||
|
||||
setVariables({
|
||||
phone: filtersObject.phone,
|
||||
name: filtersObject.name,
|
||||
email: filtersObject.email,
|
||||
address: filtersObject.address,
|
||||
id: filtersObject.id,
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
}
|
||||
|
||||
const onFilterDelete = filter => {
|
||||
const newFilters = R.filter(
|
||||
f => !R.whereEq(R.pick(['type', 'value'], f), filter),
|
||||
)(filters)
|
||||
|
||||
setFilters(newFilters)
|
||||
|
||||
const filtersObject = getFiltersObj(newFilters)
|
||||
|
||||
setVariables({
|
||||
phone: filtersObject.phone,
|
||||
name: filtersObject.name,
|
||||
email: filtersObject.email,
|
||||
address: filtersObject.address,
|
||||
id: filtersObject.id,
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
}
|
||||
|
||||
const deleteAllFilters = () => {
|
||||
setFilters([])
|
||||
const filtersObject = getFiltersObj([])
|
||||
|
||||
setVariables({
|
||||
phone: filtersObject.phone,
|
||||
name: filtersObject.name,
|
||||
email: filtersObject.email,
|
||||
address: filtersObject.address,
|
||||
id: filtersObject.id,
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
}
|
||||
|
||||
const filterOptions = R.path(['customerFilters'])(filtersResponse)
|
||||
)(customers ?? [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection
|
||||
title="Customers"
|
||||
appendix={
|
||||
<div className="flex ml-4">
|
||||
<SearchBox
|
||||
loading={loadingFilters}
|
||||
filters={filters}
|
||||
options={filterOptions}
|
||||
inputPlaceholder={'Search customers'}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
appendixRight={
|
||||
<div className="flex">
|
||||
<Link color="primary" onClick={() => setShowCreationModal(true)}>
|
||||
|
|
@ -227,21 +141,11 @@ const Customers = () => {
|
|||
{ label: 'Cash-out', icon: <TxOutIcon /> },
|
||||
]}
|
||||
/>
|
||||
{filters.length > 0 && (
|
||||
<SearchFilter
|
||||
entries={customersData.length}
|
||||
filters={filters}
|
||||
onFilterDelete={onFilterDelete}
|
||||
deleteAllFilters={deleteAllFilters}
|
||||
/>
|
||||
)}
|
||||
<CustomersList
|
||||
data={customersData}
|
||||
locale={locale}
|
||||
country={locale?.country}
|
||||
onClick={handleCustomerClicked}
|
||||
loading={customerLoading}
|
||||
triggers={triggers}
|
||||
customRequests={customRequirementsData}
|
||||
/>
|
||||
<CreateCustomerModal
|
||||
showModal={showCreationModal}
|
||||
|
|
|
|||
|
|
@ -1,78 +1,141 @@
|
|||
import Visibility from '@mui/icons-material/Visibility'
|
||||
import { format } from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
MaterialReactTable,
|
||||
MRT_ActionMenuItem,
|
||||
useMaterialReactTable,
|
||||
} from 'material-react-table'
|
||||
import { MainStatus } from '../../components/Status'
|
||||
import DataTable from '../../components/tables/DataTable'
|
||||
import TxInIcon from '../../styling/icons/direction/cash-in.svg?react'
|
||||
import TxOutIcon from '../../styling/icons/direction/cash-out.svg?react'
|
||||
import {
|
||||
defaultMaterialTableOpts,
|
||||
alignRight,
|
||||
} from '../../utils/materialReactTableOpts'
|
||||
|
||||
import { getFormattedPhone, getName } from './helper'
|
||||
|
||||
const CustomersList = ({ data, locale, onClick, loading }) => {
|
||||
const elements = [
|
||||
{
|
||||
header: 'Phone/email',
|
||||
width: 199,
|
||||
view: it => `${getFormattedPhone(it.phone, locale.country) || ''}
|
||||
${it.email || ''}`,
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
width: 241,
|
||||
view: getName,
|
||||
},
|
||||
{
|
||||
header: 'Total Txs',
|
||||
width: 126,
|
||||
textAlign: 'right',
|
||||
view: it => `${Number.parseInt(it.totalTxs)}`,
|
||||
},
|
||||
{
|
||||
header: 'Total spent',
|
||||
width: 152,
|
||||
textAlign: 'right',
|
||||
view: it =>
|
||||
`${Number.parseFloat(it.totalSpent)} ${it.lastTxFiatCode ?? ''}`,
|
||||
},
|
||||
{
|
||||
header: 'Last active',
|
||||
width: 133,
|
||||
view: it =>
|
||||
(it.lastActive && format('yyyy-MM-dd', new Date(it.lastActive))) ?? '',
|
||||
},
|
||||
{
|
||||
header: 'Last transaction',
|
||||
width: 161,
|
||||
textAlign: 'right',
|
||||
view: it => {
|
||||
const hasLastTx = !R.isNil(it.lastTxFiatCode)
|
||||
const LastTxIcon = it.lastTxClass === 'cashOut' ? TxOutIcon : TxInIcon
|
||||
const lastIcon = <LastTxIcon className="ml-3" />
|
||||
return (
|
||||
<>
|
||||
{hasLastTx &&
|
||||
`${parseFloat(it.lastTxFiat)} ${it.lastTxFiatCode ?? ''}`}
|
||||
{hasLastTx && lastIcon}
|
||||
</>
|
||||
)
|
||||
const CustomersList = ({ data, country, onClick, loading }) => {
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
size: 315,
|
||||
},
|
||||
{
|
||||
id: 'phone-email',
|
||||
accessorFn: it =>
|
||||
`${getFormattedPhone(it.phone, country) || ''} ${it.email || ''}`,
|
||||
size: 180,
|
||||
header: 'Phone/email',
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
header: 'Name',
|
||||
accessorFn: getName,
|
||||
},
|
||||
{
|
||||
accessorKey: 'totalTxs',
|
||||
header: 'Total txs',
|
||||
size: 126,
|
||||
enableColumnFilter: false,
|
||||
...alignRight,
|
||||
},
|
||||
{
|
||||
accessorKey: 'totalSpent',
|
||||
size: 152,
|
||||
enableColumnFilter: false,
|
||||
Cell: ({ cell, row }) =>
|
||||
`${Number.parseFloat(cell.getValue())} ${row.original.lastTxFiatCode ?? ''}`,
|
||||
header: 'Total spent',
|
||||
...alignRight,
|
||||
},
|
||||
{
|
||||
header: 'Last transaction',
|
||||
...alignRight,
|
||||
size: 170,
|
||||
enableColumnFilter: false,
|
||||
accessorKey: 'lastTxFiat',
|
||||
Cell: ({ cell, row }) => {
|
||||
const hasLastTx = !R.isNil(row.original.lastTxFiatCode)
|
||||
const LastTxIcon =
|
||||
row.original.lastTxClass === 'cashOut' ? TxOutIcon : TxInIcon
|
||||
const lastIcon = <LastTxIcon className="ml-3" />
|
||||
return (
|
||||
<>
|
||||
{hasLastTx &&
|
||||
`${parseFloat(cell.getValue())} ${row.original.lastTxFiatCode ?? ''}`}
|
||||
{hasLastTx && lastIcon}
|
||||
</>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastActive',
|
||||
header: 'Last active',
|
||||
size: 133,
|
||||
enableColumnFilter: false,
|
||||
Cell: ({ cell }) =>
|
||||
(cell.getValue() &&
|
||||
format('yyyy-MM-dd', new Date(cell.getValue()))) ??
|
||||
'',
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
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()]} />,
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
...defaultMaterialTableOpts,
|
||||
columns: columns,
|
||||
data,
|
||||
initialState: {
|
||||
...defaultMaterialTableOpts.initialState,
|
||||
columnVisibility: {
|
||||
id: false,
|
||||
},
|
||||
sorting: [{ id: 'lastActive', desc: true }],
|
||||
columnPinning: { right: ['mrt-row-actions'] },
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
width: 191,
|
||||
view: it => <MainStatus statuses={[it.authorizedStatus]} />,
|
||||
},
|
||||
]
|
||||
state: { isLoading: loading },
|
||||
getRowId: it => it.id,
|
||||
enableRowActions: true,
|
||||
renderRowActionMenuItems: ({ row }) => [
|
||||
<MRT_ActionMenuItem //or just use a normal MUI MenuItem component
|
||||
icon={<Visibility />}
|
||||
key="view"
|
||||
label="View"
|
||||
onClick={() => onClick(row)}
|
||||
table={table}
|
||||
/>,
|
||||
],
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
loading={loading}
|
||||
emptyText="No customers so far"
|
||||
elements={elements}
|
||||
data={data}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<MaterialReactTable table={table} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,43 +45,28 @@ const getAuthorizedStatus = (it, triggers, customRequests) => {
|
|||
)
|
||||
}
|
||||
|
||||
const pendingFieldStatus = R.map(ite => {
|
||||
if (isManualField(ite)) {
|
||||
if (uuidValidate(ite)) {
|
||||
const request = R.find(
|
||||
iter => iter.infoRequestId === ite,
|
||||
it.customInfoRequests,
|
||||
)
|
||||
return !R.isNil(request) && R.equals(request.override, 'automatic')
|
||||
const getFieldsByStatus = status =>
|
||||
R.map(ite => {
|
||||
if (isManualField(ite)) {
|
||||
if (uuidValidate(ite)) {
|
||||
const request = R.find(
|
||||
iter => iter.infoRequestId === ite,
|
||||
it.customInfoRequests,
|
||||
)
|
||||
return !R.isNil(request) && R.equals(request.override, status)
|
||||
}
|
||||
|
||||
const regularFieldValue = R.includes(ite, fieldsWithPathSuffix)
|
||||
? it[`${ite}Path`]
|
||||
: it[`${ite}`]
|
||||
if (R.isNil(regularFieldValue)) return false
|
||||
return R.equals(it[`${ite}Override`], status)
|
||||
}
|
||||
return false
|
||||
}, fields)
|
||||
|
||||
const regularFieldValue = R.includes(ite, fieldsWithPathSuffix)
|
||||
? it[`${ite}Path`]
|
||||
: it[`${ite}`]
|
||||
if (R.isNil(regularFieldValue)) return false
|
||||
return R.equals(it[`${ite}Override`], 'automatic')
|
||||
}
|
||||
return false
|
||||
}, fields)
|
||||
|
||||
const rejectedFieldStatus = R.map(ite => {
|
||||
if (isManualField(ite)) {
|
||||
if (uuidValidate(ite)) {
|
||||
const request = R.find(
|
||||
iter => iter.infoRequestId === ite,
|
||||
it.customInfoRequests,
|
||||
)
|
||||
return !R.isNil(request) && R.equals(request.override, 'blocked')
|
||||
}
|
||||
|
||||
const regularFieldValue = R.includes(ite, fieldsWithPathSuffix)
|
||||
? it[`${ite}Path`]
|
||||
: it[`${ite}`]
|
||||
if (R.isNil(regularFieldValue)) return false
|
||||
return R.equals(it[`${ite}Override`], 'blocked')
|
||||
}
|
||||
return false
|
||||
}, fields)
|
||||
const pendingFieldStatus = getFieldsByStatus('automatic')
|
||||
const rejectedFieldStatus = getFieldsByStatus('blocked')
|
||||
|
||||
if (it.authorizedOverride === CUSTOMER_BLOCKED)
|
||||
return { label: 'Blocked', type: 'error' }
|
||||
|
|
@ -235,7 +220,7 @@ const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => {
|
|||
: requirementOptions
|
||||
|
||||
const requirementName = displayRequirements
|
||||
? R.find(R.propEq('code', requirementSelected))(updatedRequirementOptions)
|
||||
? R.find(R.propEq(requirementSelected, 'code'))(updatedRequirementOptions)
|
||||
.display
|
||||
: ''
|
||||
|
||||
|
|
|
|||
323
packages/admin-ui/src/pages/Customers/helper.test.js
Normal file
323
packages/admin-ui/src/pages/Customers/helper.test.js
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { getAuthorizedStatus } from './helper'
|
||||
|
||||
describe('getAuthorizedStatus', () => {
|
||||
const mockTriggers = {
|
||||
automation: 'automatic',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const mockCustomRequests = [{ id: 'custom-req-1' }, { id: 'custom-req-2' }]
|
||||
|
||||
it('should return blocked status when authorizedOverride is blocked', () => {
|
||||
const customer = {
|
||||
authorizedOverride: 'blocked',
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(
|
||||
customer,
|
||||
mockTriggers,
|
||||
mockCustomRequests,
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Blocked',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return suspension status when customer is suspended with days > 0', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: true,
|
||||
daysSuspended: 5,
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(
|
||||
customer,
|
||||
mockTriggers,
|
||||
mockCustomRequests,
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: '5 day suspension',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return short suspension status when customer is suspended with days <= 0', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: true,
|
||||
daysSuspended: 0,
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(
|
||||
customer,
|
||||
mockTriggers,
|
||||
mockCustomRequests,
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: '< 1 day suspension',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return rejected status when any field has blocked override', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
emailOverride: 'blocked',
|
||||
email: 'test@example.com',
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'manual',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, mockCustomRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Rejected',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return pending status when any field has automatic override', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
emailOverride: 'automatic',
|
||||
email: 'test@example.com',
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'manual',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, mockCustomRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Pending',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return authorized status when no blocking conditions exist', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(
|
||||
customer,
|
||||
mockTriggers,
|
||||
mockCustomRequests,
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Authorized',
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle customers with idCardData', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
idCardData: { firstName: 'John', lastName: 'Doe' },
|
||||
idCardDataOverride: 'automatic',
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'manual',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, mockCustomRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Pending',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle customers with photo fields using path suffix', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
frontCameraPath: '/path/to/photo.jpg',
|
||||
frontCameraOverride: 'blocked',
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'manual',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, mockCustomRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Rejected',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle custom info requests with UUID validation', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
customInfoRequests: [
|
||||
{
|
||||
infoRequestId: '550e8400-e29b-41d4-a716-446655440000',
|
||||
override: 'automatic',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'manual',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const customRequests = [{ id: '550e8400-e29b-41d4-a716-446655440000' }]
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, customRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Pending',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle manual overrides for specific requirements', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
frontCameraPath: '/path/to/photo.jpg',
|
||||
frontCameraOverride: 'automatic',
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'automatic',
|
||||
overrides: [
|
||||
{
|
||||
requirement: 'facephoto',
|
||||
automation: 'manual',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, mockCustomRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Pending',
|
||||
type: 'warning',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle null or undefined triggers gracefully', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'automatic',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, mockCustomRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Authorized',
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty custom requests array', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, mockTriggers, [])
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Authorized',
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
it('should prioritize blocked status over suspension', () => {
|
||||
const customer = {
|
||||
authorizedOverride: 'blocked',
|
||||
isSuspended: true,
|
||||
daysSuspended: 5,
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(
|
||||
customer,
|
||||
mockTriggers,
|
||||
mockCustomRequests,
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Blocked',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
|
||||
it('should prioritize rejection over pending status', () => {
|
||||
const customer = {
|
||||
authorizedOverride: null,
|
||||
isSuspended: false,
|
||||
emailOverride: 'blocked',
|
||||
email: 'test@example.com',
|
||||
usSsnOverride: 'automatic',
|
||||
usSsn: '123-45-6789',
|
||||
}
|
||||
|
||||
const triggers = {
|
||||
automation: 'manual',
|
||||
overrides: [],
|
||||
}
|
||||
|
||||
const result = getAuthorizedStatus(customer, triggers, mockCustomRequests)
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Rejected',
|
||||
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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { useQuery, gql } from '@apollo/client'
|
||||
import Button from '@mui/material/Button'
|
||||
import Grid from '@mui/material/Grid'
|
||||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
|
@ -56,11 +55,9 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
|||
</Label1>
|
||||
)}
|
||||
</div>
|
||||
<Grid
|
||||
className={classnames({ 'm-0': true, 'max-h-115': showAllItems })}
|
||||
container
|
||||
spacing={1}>
|
||||
<Grid item xs={12}>
|
||||
<div
|
||||
className={classnames({ 'm-0 mt-2': true, 'max-h-115': showAllItems })}>
|
||||
<div className="w-full flex-1">
|
||||
{!alerts.length && (
|
||||
<Label1 className="text-comet -ml-1 h-30">
|
||||
No new alerts. Your system is running smoothly.
|
||||
|
|
@ -71,10 +68,10 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
|||
alerts={alerts}
|
||||
machines={machines}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
{!showAllItems && alertsLength > NUM_TO_RENDER && (
|
||||
<Grid item xs={12}>
|
||||
<div>
|
||||
<Label1 className="text-center mb-0">
|
||||
<Button
|
||||
onClick={() => onExpand('alerts')}
|
||||
|
|
@ -85,7 +82,7 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
|||
{`Show all (${alerts.length})`}
|
||||
</Button>
|
||||
</Label1>
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const Dashboard = () => {
|
|||
</TitleSection>
|
||||
<div className="flex mb-30 gap-4">
|
||||
<div className="flex flex-col flex-1">
|
||||
<Paper className="p-6">
|
||||
<Paper className="p-6 flex-1">
|
||||
<SystemPerformance />
|
||||
</Paper>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ const Footer = () => {
|
|||
const localeFiatCurrency = R.path(['locale_fiatCurrency'])(config) ?? ''
|
||||
|
||||
const renderFooterItem = key => {
|
||||
const idx = R.findIndex(R.propEq('code', key))(cryptoCurrencies)
|
||||
const idx = R.findIndex(R.propEq(key, 'code'))(cryptoCurrencies)
|
||||
const tickerCode = wallets[`${key}_ticker`]
|
||||
const tickerIdx = R.findIndex(R.propEq('code', tickerCode))(accountsConfig)
|
||||
const tickerIdx = R.findIndex(R.propEq(tickerCode, 'code'))(accountsConfig)
|
||||
|
||||
const tickerName = tickerIdx > -1 ? accountsConfig[tickerIdx].display : ''
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useQuery, gql } from '@apollo/client'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import classnames from 'classnames'
|
||||
import { isAfter } from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { Info2, Label1, Label2, P } from '../../../components/typography/index'
|
||||
|
|
@ -14,7 +13,6 @@ import { java, neon } from '../../../styling/variables'
|
|||
import { fromNamespace } from '../../../utils/config'
|
||||
import { DAY, WEEK, MONTH } from '../../../utils/time'
|
||||
import { timezones } from '../../../utils/timezone-list'
|
||||
import { toTimezone } from '../../../utils/timezones'
|
||||
|
||||
import PercentageChart from './Graphs/PercentageChart'
|
||||
import LineChart from './Graphs/RefLineChart'
|
||||
|
|
@ -27,8 +25,11 @@ BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
|||
const getFiats = R.map(R.prop('fiat'))
|
||||
|
||||
const GET_DATA = gql`
|
||||
query getData($excludeTestingCustomers: Boolean) {
|
||||
transactions(excludeTestingCustomers: $excludeTestingCustomers) {
|
||||
query getData($excludeTestingCustomers: Boolean, $from: DateTimeISO) {
|
||||
transactions(
|
||||
excludeTestingCustomers: $excludeTestingCustomers
|
||||
from: $from
|
||||
) {
|
||||
fiatCode
|
||||
fiat
|
||||
fixedFee
|
||||
|
|
@ -49,10 +50,17 @@ const GET_DATA = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const twoMonthsAgo = new Date()
|
||||
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2)
|
||||
|
||||
const SystemPerformance = () => {
|
||||
const [selectedRange, setSelectedRange] = useState('Day')
|
||||
|
||||
const { data, loading } = useQuery(GET_DATA, {
|
||||
variables: { excludeTestingCustomers: true },
|
||||
variables: {
|
||||
excludeTestingCustomers: true,
|
||||
from: twoMonthsAgo.toISOString(),
|
||||
},
|
||||
})
|
||||
const fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
|
||||
const timezone = fromNamespace('locale')(data?.config).timezone
|
||||
|
|
@ -69,38 +77,41 @@ const SystemPerformance = () => {
|
|||
if (t.error !== null) return false
|
||||
if (t.txClass === 'cashOut' && !t.dispense) return false
|
||||
if (t.txClass === 'cashIn' && !t.sendConfirmed) return false
|
||||
if (!getLastTimePeriod) {
|
||||
|
||||
const createdTimestamp = new Date(t.created).getTime()
|
||||
const [rangeStart, rangeEnd] = periodDomains[selectedRange]
|
||||
|
||||
if (getLastTimePeriod) {
|
||||
const duration = rangeEnd - rangeStart
|
||||
return (
|
||||
t.error === null &&
|
||||
isAfter(
|
||||
toTimezone(t.created, timezone),
|
||||
toTimezone(periodDomains[selectedRange][1], timezone),
|
||||
) &&
|
||||
isAfter(
|
||||
toTimezone(periodDomains[selectedRange][0], timezone),
|
||||
toTimezone(t.created, timezone),
|
||||
)
|
||||
createdTimestamp >= rangeStart - duration &&
|
||||
createdTimestamp < rangeStart
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
t.error === null &&
|
||||
isAfter(
|
||||
toTimezone(periodDomains[selectedRange][1], timezone),
|
||||
toTimezone(t.created, timezone),
|
||||
) &&
|
||||
isAfter(
|
||||
toTimezone(t.created, timezone),
|
||||
toTimezone(periodDomains[selectedRange][0], timezone),
|
||||
)
|
||||
createdTimestamp >= rangeStart &&
|
||||
createdTimestamp <= rangeEnd
|
||||
)
|
||||
}
|
||||
|
||||
const convertFiatToLocale = item => {
|
||||
if (item.fiatCode === fiatLocale) return item
|
||||
const itemRate = R.find(R.propEq('code', item.fiatCode))(data.fiatRates)
|
||||
const localeRate = R.find(R.propEq('code', fiatLocale))(data.fiatRates)
|
||||
if (item.fiatCode === fiatLocale)
|
||||
return {
|
||||
...item,
|
||||
fiat: parseFloat(item.fiat),
|
||||
profit: parseFloat(item.profit),
|
||||
}
|
||||
const itemRate = R.find(R.propEq(item.fiatCode, 'code'))(data.fiatRates)
|
||||
const localeRate = R.find(R.propEq(fiatLocale, 'code'))(data.fiatRates)
|
||||
const multiplier = localeRate.rate / itemRate.rate
|
||||
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
|
||||
return {
|
||||
...item,
|
||||
fiat: parseFloat(item.fiat) * multiplier,
|
||||
profit: parseFloat(item.profit) * multiplier,
|
||||
}
|
||||
}
|
||||
|
||||
const transactionsToShow = R.map(convertFiatToLocale)(
|
||||
|
|
@ -140,7 +151,7 @@ const SystemPerformance = () => {
|
|||
}
|
||||
|
||||
const getDirectionPercent = () => {
|
||||
const [cashIn, cashOut] = R.partition(R.propEq('txClass', 'cashIn'))(
|
||||
const [cashIn, cashOut] = R.partition(R.propEq('cashIn', 'txClass'))(
|
||||
transactionsToShow,
|
||||
)
|
||||
const totalLength = cashIn.length + cashOut.length
|
||||
|
|
@ -177,7 +188,10 @@ const SystemPerformance = () => {
|
|||
handleSetRange={setSelectedRange}
|
||||
/>
|
||||
{!loading && R.isEmpty(data.transactions) && (
|
||||
<EmptyTable className="pt-10" message="No transactions so far" />
|
||||
<EmptyTable
|
||||
className="pt-10"
|
||||
message="No transactions during the last month"
|
||||
/>
|
||||
)}
|
||||
{!loading && !R.isEmpty(data.transactions) && (
|
||||
<div className="flex flex-col gap-12">
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
|||
initialValues={locale}
|
||||
save={handleSave}
|
||||
validationSchema={LocaleSchema}
|
||||
data={R.of(locale)}
|
||||
data={R.of(Array, locale)}
|
||||
elements={mainFields(data, onChangeCoin)}
|
||||
setEditing={onEditingDefault}
|
||||
forceDisable={isEditingOverrides}
|
||||
|
|
@ -238,7 +238,7 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
|||
{wizard && (
|
||||
<Wizard
|
||||
schemas={schemas}
|
||||
coin={R.find(R.propEq('code', wizard))(cryptoCurrencies)}
|
||||
coin={R.find(R.propEq(wizard, 'code'))(cryptoCurrencies)}
|
||||
onClose={() => setWizard(false)}
|
||||
save={wizardSave}
|
||||
error={error?.message}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const allFields = (getData, onChange, auxElements = []) => {
|
|||
|
||||
return R.compose(
|
||||
it => `${R.prop(code)(it)} ${it?.isBeta ? '(Beta)' : ''}`,
|
||||
R.find(R.propEq(compare ?? 'code', it)),
|
||||
R.find(R.propEq(it, compare ?? 'code')),
|
||||
)(data)
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ const allFields = (getData, onChange, auxElements = []) => {
|
|||
const timezonesData = timezoneList
|
||||
|
||||
const findSuggestion = it => {
|
||||
const machine = R.find(R.propEq('deviceId', it.machine))(machineData)
|
||||
const machine = R.find(R.propEq(it.machine, 'deviceId'))(machineData)
|
||||
return machine ? [machine] : []
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ const Commissions = ({ name: SCREEN_KEY, id: deviceId }) => {
|
|||
|
||||
const overrides = config.overrides
|
||||
? R.concat(
|
||||
R.filter(R.propEq('machine', 'ALL_MACHINES'), config.overrides),
|
||||
R.filter(R.propEq('machine', deviceId), config.overrides),
|
||||
R.filter(R.propEq('ALL_MACHINES', 'machine'), config.overrides),
|
||||
R.filter(R.propEq(deviceId, 'machine'), config.overrides),
|
||||
)
|
||||
: []
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ const Machines = ({ data, refetch, reload }) => {
|
|||
const machineID = R.path(['deviceId'])(machine) ?? null
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 h-full gap-12">
|
||||
<div className="flex flex-1 h-full gap-12 mb-12">
|
||||
<div className="basis-1/4 min-w-1/4 pt-8">
|
||||
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
|
||||
<Link to="/dashboard" className="no-underline">
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ const CashCassettes = () => {
|
|||
/>
|
||||
{wizard && (
|
||||
<Wizard
|
||||
machine={R.find(R.propEq('id', machineId), machines)}
|
||||
machine={R.find(R.propEq(machineId, 'id'), machines)}
|
||||
cashoutSettings={getCashoutSettings(machineId)}
|
||||
onClose={() => {
|
||||
setWizard(false)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
|
|||
textAlign: 'left',
|
||||
view: R.pipe(
|
||||
R.prop('deviceId'),
|
||||
id => R.find(R.propEq('id', id), machines),
|
||||
id => R.find(R.propEq(id, 'id'), machines),
|
||||
R.defaultTo({ name: <i>Unpaired device</i> }),
|
||||
R.prop('name'),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const MachineStatus = () => {
|
|||
]
|
||||
|
||||
const machines = R.path(['machines'])(machinesResponse) ?? []
|
||||
const expandedIndex = R.findIndex(R.propEq('deviceId', addedMachineId))(
|
||||
const expandedIndex = R.findIndex(R.propEq(addedMachineId, 'deviceId'))(
|
||||
machines,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
R.pipe(R.pickAll(fields), R.map(defaultToZero))(cassetteInput)
|
||||
|
||||
const onContinue = it => {
|
||||
const newConfig = R.merge(config, it)
|
||||
const newConfig = R.mergeRight(config, it)
|
||||
if (isLastStep) {
|
||||
const wasCashboxEmptied = [
|
||||
config?.wasCashboxEmptied,
|
||||
|
|
@ -158,7 +158,7 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
|||
: {}
|
||||
|
||||
const makeInitialValues = () =>
|
||||
R.merge(makeCassettesInitialValues(), makeRecyclersInitialValues())
|
||||
R.mergeRight(makeCassettesInitialValues(), makeRecyclersInitialValues())
|
||||
|
||||
const steps = R.pipe(
|
||||
R.concat(
|
||||
|
|
|
|||
|
|
@ -37,12 +37,12 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
|
||||
const overriddenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(setupValues)
|
||||
const suggestionFilter = R.filter(
|
||||
it => !R.contains(it.code, overriddenCryptos),
|
||||
it => !R.includes(it.code, overriddenCryptos),
|
||||
)
|
||||
const suggestions = suggestionFilter(cryptoCurrencies)
|
||||
|
||||
const findSuggestion = it => {
|
||||
const coin = R.compose(R.find(R.propEq('code', it?.cryptoCurrency)))(
|
||||
const coin = R.compose(R.find(R.propEq(it?.cryptoCurrency, 'code')))(
|
||||
cryptoCurrencies,
|
||||
)
|
||||
return coin ? [coin] : []
|
||||
|
|
@ -90,7 +90,7 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
const viewCrypto = it =>
|
||||
R.compose(
|
||||
R.path(['display']),
|
||||
R.find(R.propEq('code', it)),
|
||||
R.find(R.propEq(it, 'code')),
|
||||
)(cryptoCurrencies)
|
||||
|
||||
const elements = [
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
|||
)
|
||||
|
||||
const findSuggestion = it => {
|
||||
const coin = R.find(R.propEq('deviceId', it?.machine), machines)
|
||||
const coin = R.find(R.propEq(it?.machine, 'deviceId'), machines)
|
||||
return coin ? [coin] : []
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
|||
)
|
||||
|
||||
const viewMachine = it =>
|
||||
R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines)
|
||||
R.compose(R.path(['name']), R.find(R.propEq(it, 'deviceId')))(machines)
|
||||
|
||||
const elements = R.concat(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const ThirdPartyProvider = () => {
|
|||
const getDisplayName = type => it =>
|
||||
R.compose(
|
||||
R.prop('display'),
|
||||
R.find(R.propEq('code', it)),
|
||||
R.find(R.propEq(it, 'code')),
|
||||
)(filterOptions(type))
|
||||
|
||||
const innerSave = async value => {
|
||||
|
|
@ -73,7 +73,7 @@ const ThirdPartyProvider = () => {
|
|||
<EditableTable
|
||||
name="thirdParty"
|
||||
initialValues={values}
|
||||
data={R.of(values)}
|
||||
data={R.of(Array, values)}
|
||||
error={error?.message}
|
||||
enableEdit
|
||||
editWidth={174}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ const ContactInfo = ({ wizard }) => {
|
|||
},
|
||||
]
|
||||
|
||||
const findField = name => R.find(R.propEq('name', name))(fields)
|
||||
const findField = name => R.find(R.propEq(name, 'name'))(fields)
|
||||
const findValue = name => findField(name).value
|
||||
|
||||
const displayTextValue = value => value
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ const TermsConditions = () => {
|
|||
},
|
||||
]
|
||||
|
||||
const findField = name => R.find(R.propEq('name', name))(fields)
|
||||
const findField = name => R.find(R.propEq(name, 'name'))(fields)
|
||||
const findValue = name => findField(name).value
|
||||
|
||||
const initialValues = {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const FormRenderer = ({
|
|||
R.map(({ code }) => ({ [code]: (value && value[code]) ?? '' })),
|
||||
)(elements)
|
||||
|
||||
const values = R.merge(initialValues, value)
|
||||
const values = R.mergeRight(initialValues, value)
|
||||
|
||||
const [saveError, setSaveError] = useState([])
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const Services = () => {
|
|||
|
||||
const getAccounts = ({ elements, code }) => {
|
||||
const account = accounts[code]
|
||||
const filterBySecretComponent = R.filter(R.propEq('component', SecretInput))
|
||||
const filterBySecretComponent = R.filter(R.propEq(SecretInput, 'component'))
|
||||
const mapToCode = R.map(R.prop(['code']))
|
||||
const passwordFields = R.compose(
|
||||
mapToCode,
|
||||
|
|
|
|||
|
|
@ -70,13 +70,13 @@ const Triggers = () => {
|
|||
const enabledCustomInfoRequests = R.pipe(
|
||||
R.path(['customInfoRequests']),
|
||||
R.defaultTo([]),
|
||||
R.filter(R.propEq('enabled', true)),
|
||||
R.filter(R.propEq(true, 'enabled')),
|
||||
)(customInfoReqData)
|
||||
|
||||
const emailAuth =
|
||||
data?.config?.triggersConfig_customerAuthentication === 'EMAIL'
|
||||
|
||||
const complianceServices = R.filter(R.propEq('class', 'compliance'))(
|
||||
const complianceServices = R.filter(R.propEq('compliance', 'class'))(
|
||||
data?.accountsConfig || [],
|
||||
)
|
||||
const triggers = fromServer(data?.config?.triggers ?? [])
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ const Wizard = ({
|
|||
)
|
||||
|
||||
const onContinue = async it => {
|
||||
const newConfig = R.merge(config, stepOptions.schema.cast(it))
|
||||
const newConfig = R.mergeRight(config, stepOptions.schema.cast(it))
|
||||
|
||||
if (isLastStep) {
|
||||
return save(newConfig)
|
||||
|
|
@ -221,7 +221,7 @@ const Wizard = ({
|
|||
|
||||
const createErrorMessage = (errors, touched, values) => {
|
||||
const triggerType = values?.triggerType
|
||||
const containsType = R.contains(triggerType)
|
||||
const containsType = R.includes(triggerType)
|
||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||
const isCustom = values?.requirement?.requirement === 'custom'
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const AdvancedTriggersSettings = memo(() => {
|
|||
|
||||
const customInfoRequests =
|
||||
R.path(['customInfoRequests'])(customInfoReqData) ?? []
|
||||
const enabledCustomInfoRequests = R.filter(R.propEq('enabled', true))(
|
||||
const enabledCustomInfoRequests = R.filter(R.propEq(true, 'enabled'))(
|
||||
customInfoRequests,
|
||||
)
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ const AdvancedTriggersSettings = memo(() => {
|
|||
initialValues={requirementsDefaults}
|
||||
save={saveDefaults}
|
||||
validationSchema={defaultSchema}
|
||||
data={R.of(requirementsDefaults)}
|
||||
data={R.of(Array, requirementsDefaults)}
|
||||
elements={getDefaultSettings()}
|
||||
setEditing={onEditingDefault}
|
||||
forceDisable={isEditingOverrides}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const buildAdvancedRequirementOptions = customInfoRequests => {
|
|||
const displayRequirement = (code, customInfoRequests) => {
|
||||
return R.prop(
|
||||
'display',
|
||||
R.find(R.propEq('code', code))(
|
||||
R.find(R.propEq(code, 'code'))(
|
||||
buildAdvancedRequirementOptions(customInfoRequests),
|
||||
),
|
||||
)
|
||||
|
|
@ -47,7 +47,7 @@ const getOverridesSchema = (values, customInfoRequests) => {
|
|||
const { id, requirement } = this.parent
|
||||
// If we're editing, filter out the override being edited so that validation schemas don't enter in circular conflicts
|
||||
const _values = R.filter(it => it.id !== id, values)
|
||||
if (R.find(R.propEq('requirement', requirement))(_values)) {
|
||||
if (R.find(R.propEq(requirement, 'requirement'))(_values)) {
|
||||
return this.createError({
|
||||
message: `Requirement '${displayRequirement(
|
||||
requirement,
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ const Type = ({ ...props }) => {
|
|||
'text-tomato': errors.triggerType && touched.triggerType,
|
||||
}
|
||||
|
||||
const containsType = R.contains(values?.triggerType)
|
||||
const containsType = R.includes(values?.triggerType)
|
||||
const isThresholdCurrencyEnabled = containsType(['txAmount', 'txVolume'])
|
||||
const isTransactionAmountEnabled = containsType(['txVelocity'])
|
||||
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
||||
|
|
@ -542,7 +542,7 @@ const requirements = (
|
|||
const getView = (data, code, compare) => it => {
|
||||
if (!data) return ''
|
||||
|
||||
return R.compose(R.prop(code), R.find(R.propEq(compare ?? 'code', it)))(data)
|
||||
return R.compose(R.prop(code), R.find(R.propEq(it, compare ?? 'code')))(data)
|
||||
}
|
||||
|
||||
const customReqIdMatches = customReqId => it => {
|
||||
|
|
|
|||
|
|
@ -69,12 +69,12 @@ const AdvancedWallet = () => {
|
|||
AdvancedWalletSettingsOverrides,
|
||||
)
|
||||
const suggestionFilter = R.filter(
|
||||
it => !R.contains(it.code, overriddenCryptos),
|
||||
it => !R.includes(it.code, overriddenCryptos),
|
||||
)
|
||||
const coinSuggestions = suggestionFilter(cryptoCurrencies)
|
||||
|
||||
const findSuggestion = it => {
|
||||
const coin = R.compose(R.find(R.propEq('code', it?.cryptoCurrency)))(
|
||||
const coin = R.compose(R.find(R.propEq(it?.cryptoCurrency, 'code')))(
|
||||
cryptoCurrencies,
|
||||
)
|
||||
return coin ? [coin] : []
|
||||
|
|
@ -85,13 +85,13 @@ const AdvancedWallet = () => {
|
|||
<Section>
|
||||
<EditableTable
|
||||
name="wallets"
|
||||
data={R.of(AdvancedWalletSettings)}
|
||||
data={R.of(Array, AdvancedWalletSettings)}
|
||||
error={error?.message}
|
||||
enableEdit
|
||||
editWidth={174}
|
||||
save={save}
|
||||
stripeWhen={it => !AdvancedWalletSchema.isValidSync(it)}
|
||||
inialValues={R.of(AdvancedWalletSettings)}
|
||||
inialValues={R.of(Array, AdvancedWalletSettings)}
|
||||
validationSchema={AdvancedWalletSchema}
|
||||
elements={getAdvancedWalletElements()}
|
||||
setEditing={onEditingDefault}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
|||
/>
|
||||
{wizard && (
|
||||
<Wizard
|
||||
coin={R.find(R.propEq('code', wizard))(cryptoCurrencies)}
|
||||
coin={R.find(R.propEq(wizard, 'code'))(cryptoCurrencies)}
|
||||
onClose={() => setWizard(false)}
|
||||
save={save}
|
||||
schemas={schemas}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import { has0Conf } from './helper'
|
|||
const MAX_STEPS = 5
|
||||
const MODAL_WIDTH = 554
|
||||
|
||||
const contains = crypto => R.compose(R.contains(crypto), R.prop('cryptos'))
|
||||
const sameClass = type => R.propEq('class', type)
|
||||
const contains = crypto => R.compose(R.includes(crypto), R.prop('cryptos'))
|
||||
const sameClass = type => R.propEq(type, 'class')
|
||||
const filterConfig = (crypto, type) =>
|
||||
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
|
||||
const removeDeprecated = R.filter(({ deprecated }) => !deprecated)
|
||||
|
|
@ -59,7 +59,7 @@ const Wizard = ({
|
|||
const exchanges = getItems(accountsConfig, accounts, 'exchange', coin.code)
|
||||
const zeroConfs = getItems(accountsConfig, accounts, 'zeroConf', coin.code)
|
||||
|
||||
const getValue = code => R.find(R.propEq('code', code))(accounts)
|
||||
const getValue = code => R.find(R.propEq(code, 'code'))(accounts)
|
||||
|
||||
const commonWizardSteps = [
|
||||
{ type: 'ticker', ...tickers },
|
||||
|
|
@ -99,9 +99,9 @@ const Wizard = ({
|
|||
const stepData = step > 0 ? wizardSteps[step - 1] : null
|
||||
|
||||
const onContinue = async (stepConfig, stepAccount) => {
|
||||
const newConfig = R.merge(config, stepConfig)
|
||||
const newConfig = R.mergeRight(config, stepConfig)
|
||||
const newAccounts = stepAccount
|
||||
? R.merge(accountsToSave, stepAccount)
|
||||
? R.mergeRight(accountsToSave, stepAccount)
|
||||
: accountsToSave
|
||||
|
||||
if (isLastStep) {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const reducer = (state, action) => {
|
|||
iError: false,
|
||||
}
|
||||
case 'error':
|
||||
return R.merge(state, { innerError: true })
|
||||
return R.mergeRight(state, { innerError: true })
|
||||
case 'reset':
|
||||
return initialState
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { CURRENCY_MAX } from '../../utils/constants'
|
|||
import { defaultToZero } from '../../utils/number'
|
||||
|
||||
const filterClass = type => R.filter(it => it.class === type)
|
||||
const filterCoins = ({ id }) => R.filter(it => R.contains(id)(it.cryptos))
|
||||
const filterCoins = ({ id }) => R.filter(it => R.includes(id)(it.cryptos))
|
||||
|
||||
const WalletSchema = Yup.object().shape({
|
||||
ticker: Yup.string('The ticker must be a string').required(
|
||||
|
|
@ -36,6 +36,7 @@ const AdvancedWalletSchema = Yup.object().shape({
|
|||
cryptoUnits: Yup.string().required(),
|
||||
feeMultiplier: Yup.string().required(),
|
||||
allowTransactionBatching: Yup.boolean(),
|
||||
enableLastUsedAddress: Yup.boolean(),
|
||||
})
|
||||
|
||||
const OverridesSchema = Yup.object().shape({
|
||||
|
|
@ -57,7 +58,7 @@ const OverridesDefaults = {
|
|||
}
|
||||
|
||||
const viewFeeMultiplier = it =>
|
||||
R.compose(R.prop(['display']), R.find(R.propEq('code', it)))(feeOptions)
|
||||
R.compose(R.prop(['display']), R.find(R.propEq(it, 'code')))(feeOptions)
|
||||
|
||||
const feeOptions = [
|
||||
{ display: '+60%', code: '1.6' },
|
||||
|
|
@ -127,6 +128,17 @@ const getAdvancedWalletElements = () => {
|
|||
labelProp: 'display',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'enableLastUsedAddress',
|
||||
header: `Allow last used address prompt`,
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
width: 260,
|
||||
view: (_, ite) => {
|
||||
return ite.enableLastUsedAddress ? 'Yes' : `No`
|
||||
},
|
||||
input: Checkbox,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +216,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
|||
const viewCryptoCurrency = it => {
|
||||
const currencyDisplay = R.compose(
|
||||
it => `${R.prop(['display'])(it)} ${it?.isBeta ? '(Beta)' : ''}`,
|
||||
R.find(R.propEq('code', it)),
|
||||
R.find(R.propEq(it, 'code')),
|
||||
)(cryptoCurrencies)
|
||||
return currencyDisplay
|
||||
}
|
||||
|
|
@ -213,7 +225,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
|||
const getDisplayName = type => it =>
|
||||
R.compose(
|
||||
R.prop('display'),
|
||||
R.find(R.propEq('code', it)),
|
||||
R.find(R.propEq(it, 'code')),
|
||||
)(filterOptions(type))
|
||||
|
||||
const getOptions = R.curry((option, it) =>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const SAVE_ACCOUNTS = gql`
|
|||
`
|
||||
|
||||
const isConfigurable = it =>
|
||||
!R.isNil(it) && !R.contains(it)(['mock-exchange', 'no-exchange'])
|
||||
!R.isNil(it) && !R.includes(it)(['mock-exchange', 'no-exchange'])
|
||||
|
||||
const ChooseExchange = ({ data: currentData, addData }) => {
|
||||
const { data } = useQuery(GET_CONFIG)
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ const SAVE_ACCOUNTS = gql`
|
|||
`
|
||||
|
||||
const isConfigurable = it =>
|
||||
R.contains(it)(['infura', 'bitgo', 'trongrid', 'galoy'])
|
||||
R.includes(it)(['infura', 'bitgo', 'trongrid', 'galoy'])
|
||||
|
||||
const isLocalHosted = it =>
|
||||
R.contains(it)([
|
||||
R.includes(it)([
|
||||
'bitcoind',
|
||||
'geth',
|
||||
'litecoind',
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const Wallet = ({ doContinue }) => {
|
|||
const Component = mySteps[step].component
|
||||
|
||||
const addData = it => {
|
||||
setData(R.merge(data, it))
|
||||
setData(R.mergeRight(data, it))
|
||||
setStep(step + 1)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import * as R from 'ramda'
|
|||
|
||||
import _schema from '../../../Services/schemas'
|
||||
|
||||
const contains = crypto => R.compose(R.contains(crypto), R.prop('cryptos'))
|
||||
const sameClass = type => R.propEq('class', type)
|
||||
const contains = crypto => R.compose(R.includes(crypto), R.prop('cryptos'))
|
||||
const sameClass = type => R.propEq(type, 'class')
|
||||
const filterConfig = (crypto, type) =>
|
||||
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
|
||||
export const getItems = (accountsConfig, accounts, type, crypto) => {
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ const hasSidebar = route =>
|
|||
const getParent = route =>
|
||||
R.find(
|
||||
R.propEq(
|
||||
'route',
|
||||
R.dropLast(
|
||||
1,
|
||||
R.dropLastWhile(x => x !== '/', route),
|
||||
),
|
||||
'route',
|
||||
),
|
||||
)(flattened)
|
||||
|
||||
|
|
@ -69,8 +69,8 @@ const Routes = () => {
|
|||
Transition === Slide
|
||||
? {
|
||||
direction:
|
||||
R.findIndex(R.propEq('route', history.state.prev))(leafRoutes) >
|
||||
R.findIndex(R.propEq('route', location))(leafRoutes)
|
||||
R.findIndex(R.propEq(history.state.prev, 'route'))(leafRoutes) >
|
||||
R.findIndex(R.propEq(location, 'route'))(leafRoutes)
|
||||
? 'right'
|
||||
: 'left',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ const { p } = typographyStyles
|
|||
let theme = createTheme({
|
||||
typography: {
|
||||
fontFamily: inputFontFamily,
|
||||
root: { ...p },
|
||||
body1: { ...p },
|
||||
},
|
||||
palette: {
|
||||
primary: {
|
||||
|
|
@ -56,6 +58,18 @@ theme = createTheme(theme, {
|
|||
body1: { ...p },
|
||||
},
|
||||
},
|
||||
MuiCircularProgress: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: primaryColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: { ...p },
|
||||
},
|
||||
},
|
||||
MuiIconButtonBase: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
|
|
|
|||
33
packages/admin-ui/src/utils/materialReactTableOpts.js
Normal file
33
packages/admin-ui/src/utils/materialReactTableOpts.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
const defaultMaterialTableOpts = {
|
||||
enableGlobalFilter: false,
|
||||
paginationDisplayMode: 'pages',
|
||||
enableColumnActions: false,
|
||||
initialState: { density: 'compact' },
|
||||
mrtTheme: it => ({
|
||||
...it,
|
||||
baseBackgroundColor: '#fff',
|
||||
}),
|
||||
muiTopToolbarProps: () => ({
|
||||
sx: {
|
||||
backgroundColor: 'var(--zodiac)',
|
||||
'& .MuiButtonBase-root': { color: '#fff' },
|
||||
},
|
||||
}),
|
||||
muiTableHeadRowProps: () => ({
|
||||
sx: { backgroundColor: 'var(--zircon)' },
|
||||
}),
|
||||
}
|
||||
|
||||
const alignRight = {
|
||||
muiTableHeadCellProps: {
|
||||
align: 'right',
|
||||
},
|
||||
muiTableBodyCellProps: {
|
||||
align: 'right',
|
||||
},
|
||||
muiTableFooterCellProps: {
|
||||
align: 'right',
|
||||
},
|
||||
}
|
||||
|
||||
export { defaultMaterialTableOpts, alignRight }
|
||||
36
packages/server/bin/lamassu-batch-diagnostics
Normal file
36
packages/server/bin/lamassu-batch-diagnostics
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const db = require('../lib/db')
|
||||
const machineLoader = require('../lib/machine-loader')
|
||||
const operator = require('../lib/operator')
|
||||
|
||||
console.log('Running diagnostics on all paired devices...\n')
|
||||
|
||||
operator.getOperatorId('middleware')
|
||||
.then(operatorId => {
|
||||
if (!operatorId) {
|
||||
throw new Error('Operator ID not found in database')
|
||||
}
|
||||
|
||||
return db.any('SELECT device_id, name FROM devices')
|
||||
.then(devices => ({ operatorId, devices }))
|
||||
})
|
||||
.then(({ operatorId, devices }) => {
|
||||
if (devices.length === 0) {
|
||||
console.log('No paired devices found.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const deviceIds = devices.map(d => d.device_id)
|
||||
return machineLoader.batchDiagnostics(deviceIds, operatorId)
|
||||
})
|
||||
.then(() => {
|
||||
console.log('\n✓ Diagnostics initiated for all devices. It can take a few minutes for the results to appear on the admin.')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error:', err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -13,11 +13,32 @@ const createMigration = `CREATE TABLE IF NOT EXISTS migrations (
|
|||
// no need to log the migration process
|
||||
process.env.SKIP_SERVER_LOGS = true
|
||||
|
||||
db.none(createMigration)
|
||||
.then(() => migrate.run())
|
||||
.then(() => {
|
||||
console.log('DB Migration succeeded.')
|
||||
process.exit(0)
|
||||
function checkPostgresVersion () {
|
||||
return db.one('SHOW server_version;')
|
||||
.then(result => {
|
||||
console.log(result)
|
||||
const versionString = result.server_version
|
||||
const match = versionString.match(/(\d+)\.(\d+)/i)
|
||||
if (!match) {
|
||||
throw new Error(`Could not parse PostgreSQL version: ${versionString}`)
|
||||
}
|
||||
return parseInt(match[1], 10)
|
||||
})
|
||||
}
|
||||
|
||||
checkPostgresVersion()
|
||||
.then(majorVersion => {
|
||||
if (majorVersion < 12) {
|
||||
console.error('PostgreSQL version must be 12 or higher. Current version:', majorVersion)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return db.none(createMigration)
|
||||
.then(() => migrate.run())
|
||||
.then(() => {
|
||||
console.log('DB Migration succeeded.')
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('DB Migration failed: %s', err)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const logger = require('../logger')
|
|||
const settingsLoader = require('../new-settings-loader')
|
||||
const configManager = require('../new-config-manager')
|
||||
const notifier = require('../notifier')
|
||||
const constants = require('../constants')
|
||||
|
||||
const cashInAtomic = require('./cash-in-atomic')
|
||||
const cashInLow = require('./cash-in-low')
|
||||
|
|
@ -194,14 +195,27 @@ function postProcess(r, pi, isBlacklisted, addressReuse, walletScore) {
|
|||
})
|
||||
}
|
||||
|
||||
// This feels like it can be simplified,
|
||||
// but it's the most concise query to express the requirement and its edge cases.
|
||||
// At most only one authenticated customer can use an address.
|
||||
// If the current customer is anon, we can still allow one other customer to use the address,
|
||||
// So we count distinct customers plus the current customer if they are not anonymous.
|
||||
// To prevent malicious blocking of address, we only check for txs with actual fiat
|
||||
function doesTxReuseAddress(tx) {
|
||||
const sql = `
|
||||
SELECT EXISTS (
|
||||
SELECT DISTINCT to_address FROM (
|
||||
SELECT to_address FROM cash_in_txs WHERE id != $1
|
||||
) AS x WHERE to_address = $2
|
||||
)`
|
||||
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
|
||||
SELECT COUNT(*) > 1 as exists
|
||||
FROM (SELECT DISTINCT customer_id
|
||||
FROM cash_in_txs
|
||||
WHERE to_address = $1
|
||||
AND customer_id != $3
|
||||
AND fiat > 0
|
||||
UNION
|
||||
SELECT $2
|
||||
WHERE $2 != $3) t;
|
||||
`
|
||||
return db
|
||||
.one(sql, [tx.toAddress, tx.customerId, constants.anonymousCustomer.uuid])
|
||||
.then(({ exists }) => exists)
|
||||
}
|
||||
|
||||
function getWalletScore(tx, pi) {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@
|
|||
*/
|
||||
const prepare_denominations = denominations =>
|
||||
JSON.parse(JSON.stringify(denominations))
|
||||
.sort(([d1], [d2]) => d1 < d2)
|
||||
.sort(([d1], [d2]) => d2 - d1)
|
||||
.reduce(
|
||||
([csum, denoms], [denom, count]) => {
|
||||
csum += denom * count
|
||||
return [csum, [{ denom, count, csum }].concat(denoms)]
|
||||
denoms.push({ denom, count, csum })
|
||||
return [csum, denoms]
|
||||
},
|
||||
[0, []],
|
||||
)[1] /* ([csum, denoms]) => denoms */
|
||||
|
|
|
|||
|
|
@ -8,16 +8,17 @@ 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')
|
||||
const notifierUtils = require('./notifier/utils')
|
||||
const NUM_RESULTS = 1000
|
||||
const sms = require('./sms')
|
||||
const settingsLoader = require('./new-settings-loader')
|
||||
const logger = require('./logger')
|
||||
const externalCompliance = require('./compliance-external')
|
||||
const {
|
||||
customers: { getCustomerList },
|
||||
} = require('typesafe-db')
|
||||
|
||||
const { APPROVED, RETRY } = require('./plugins/compliance/consts')
|
||||
|
||||
|
|
@ -483,28 +484,6 @@ function addComplianceOverrides(id, customer, userToken) {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all customers
|
||||
*
|
||||
* Add status as computed column,
|
||||
* which will indicate the name of the latest
|
||||
* compliance verfication completed by user.
|
||||
*
|
||||
* @returns {array} Array of customers populated with status field
|
||||
*/
|
||||
function batch() {
|
||||
const sql = `select * from customers
|
||||
where id != $1
|
||||
order by created desc limit $2`
|
||||
return db.any(sql, [anonymous.uuid, NUM_RESULTS]).then(customers =>
|
||||
Promise.all(
|
||||
_.map(customer => {
|
||||
return getCustomInfoRequestsData(customer).then(camelize)
|
||||
}, customers),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function getSlimCustomerByIdBatch(ids) {
|
||||
const sql = `SELECT id, phone, id_card_data
|
||||
FROM customers
|
||||
|
|
@ -512,88 +491,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,
|
||||
NUM_RESULTS,
|
||||
phone,
|
||||
name,
|
||||
address,
|
||||
id,
|
||||
email,
|
||||
])
|
||||
.then(customers =>
|
||||
Promise.all(
|
||||
_.map(
|
||||
customer => getCustomInfoRequestsData(customer).then(camelizeDeep),
|
||||
customers,
|
||||
),
|
||||
),
|
||||
)
|
||||
function getCustomersList() {
|
||||
return getCustomerList({ withCustomInfoRequest: true })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1081,12 +980,10 @@ function notifyApprovedExternalCompliance(settings, customerId) {
|
|||
|
||||
function checkExternalCompliance(settings) {
|
||||
return getOpenExternalCompliance().then(externals => {
|
||||
console.log(externals)
|
||||
const promises = _.map(external => {
|
||||
return externalCompliance
|
||||
.getStatus(settings, external.service, external.customer_id)
|
||||
.then(status => {
|
||||
console.log('status', status, external.customer_id, external.service)
|
||||
if (status.status.answer === RETRY)
|
||||
notifyRetryExternalCompliance(
|
||||
settings,
|
||||
|
|
@ -1112,12 +1009,16 @@ function addExternalCompliance(customerId, service, id) {
|
|||
return db.none(sql, [customerId, id, service])
|
||||
}
|
||||
|
||||
function getLastUsedAddress(id, cryptoCode) {
|
||||
const sql = `SELECT to_address FROM cash_in_txs WHERE customer_id=$1 AND crypto_code=$2 AND fiat > 0 ORDER BY created DESC LIMIT 1`
|
||||
return db.oneOrNone(sql, [id, cryptoCode]).then(it => it?.to_address)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
add,
|
||||
addWithEmail,
|
||||
get,
|
||||
getWithEmail,
|
||||
batch,
|
||||
getSlimCustomerByIdBatch,
|
||||
getCustomersList,
|
||||
getCustomerById,
|
||||
|
|
@ -1139,4 +1040,5 @@ module.exports = {
|
|||
updateLastAuthAttempt,
|
||||
addExternalCompliance,
|
||||
checkExternalCompliance,
|
||||
getLastUsedAddress,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ const eventBus = require('./event-bus')
|
|||
const DATABASE_NOT_REACHABLE = 'Database not reachable.'
|
||||
|
||||
const pgp = Pgp({
|
||||
pgNative: true,
|
||||
schema: 'public',
|
||||
error: (err, e) => {
|
||||
if (e.cn) logger.error(DATABASE_NOT_REACHABLE)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function getBitPayFxRate(
|
|||
fiatCodeProperty,
|
||||
rateProperty,
|
||||
) {
|
||||
return getFiatRates().then(({ data: fxRates }) => {
|
||||
return getFiatRates().then(fxRates => {
|
||||
const defaultFiatRate = findCurrencyRates(
|
||||
fxRates,
|
||||
defaultFiatMarket,
|
||||
|
|
@ -69,14 +69,15 @@ const getRate = (retries = 1, fiatCode, defaultFiatMarket) => {
|
|||
defaultFiatMarket,
|
||||
fiatCodeProperty,
|
||||
rateProperty,
|
||||
).catch(() => {
|
||||
// Switch service
|
||||
).catch(err => {
|
||||
const erroredService = API_QUEUE.shift()
|
||||
API_QUEUE.push(erroredService)
|
||||
if (retries >= MAX_ROTATIONS)
|
||||
throw new Error(`FOREX API error from ${erroredService.name}`)
|
||||
throw new Error(
|
||||
`FOREX API error from ${erroredService.name} ${err?.message}`,
|
||||
)
|
||||
|
||||
return getRate(++retries, fiatCode)
|
||||
return getRate(++retries, fiatCode, defaultFiatMarket)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -523,6 +523,43 @@ function diagnostics(rec) {
|
|||
)
|
||||
}
|
||||
|
||||
function batchDiagnostics(deviceIds, operatorId) {
|
||||
const diagnosticsDir = `${OPERATOR_DATA_DIR}/diagnostics/`
|
||||
|
||||
const removeDir = fsPromises
|
||||
.rm(diagnosticsDir, { recursive: true })
|
||||
.catch(err => {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
const sql = `UPDATE devices
|
||||
SET diagnostics_timestamp = NULL,
|
||||
diagnostics_scan_updated_at = NULL,
|
||||
diagnostics_front_updated_at = NULL
|
||||
WHERE device_id = ANY($1)`
|
||||
|
||||
// Send individual notifications for each machine
|
||||
const sendNotifications = deviceIds.map(deviceId =>
|
||||
db.none('NOTIFY $1:name, $2', [
|
||||
'machineAction',
|
||||
JSON.stringify({
|
||||
action: 'diagnostics',
|
||||
value: {
|
||||
deviceId,
|
||||
operatorId,
|
||||
action: 'diagnostics',
|
||||
},
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
||||
return removeDir
|
||||
.then(() => db.none(sql, [deviceIds]))
|
||||
.then(() => Promise.all(sendNotifications))
|
||||
}
|
||||
|
||||
function setMachine(rec, operatorId) {
|
||||
rec.operatorId = operatorId
|
||||
switch (rec.action) {
|
||||
|
|
@ -681,4 +718,5 @@ module.exports = {
|
|||
refillMachineUnits,
|
||||
updateDiagnostics,
|
||||
updateFailedQRScans,
|
||||
batchDiagnostics,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,18 +27,5 @@ function transaction() {
|
|||
|
||||
return db.any(sql)
|
||||
}
|
||||
function customer() {
|
||||
const sql = `SELECT DISTINCT * FROM (
|
||||
SELECT 'phone' AS type, phone AS value FROM customers WHERE phone IS NOT NULL UNION
|
||||
SELECT 'email' AS type, email AS value FROM customers WHERE email IS NOT NULL UNION
|
||||
SELECT 'name' AS type, id_card_data::json->>'firstName' AS value FROM customers WHERE id_card_data::json->>'firstName' IS NOT NULL AND id_card_data::json->>'lastName' IS NULL UNION
|
||||
SELECT 'name' AS type, id_card_data::json->>'lastName' AS value FROM customers WHERE id_card_data::json->>'firstName' IS NULL AND id_card_data::json->>'lastName' IS NOT NULL UNION
|
||||
SELECT 'name' AS type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') AS value FROM customers WHERE id_card_data::json->>'firstName' IS NOT NULL AND id_card_data::json->>'lastName' IS NOT NULL UNION
|
||||
SELECT 'address' as type, id_card_data::json->>'address' AS value FROM customers WHERE id_card_data::json->>'address' IS NOT NULL UNION
|
||||
SELECT 'id' AS type, id_card_data::json->>'documentNumber' AS value FROM customers WHERE id_card_data::json->>'documentNumber' IS NOT NULL
|
||||
) f`
|
||||
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
module.exports = { transaction, customer }
|
||||
module.exports = { transaction }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
const authentication = require('../modules/userManagement')
|
||||
const anonymous = require('../../../constants').anonymousCustomer
|
||||
const customers = require('../../../customers')
|
||||
const filters = require('../../filters')
|
||||
const customerNotes = require('../../../customer-notes')
|
||||
const machineLoader = require('../../../machine-loader')
|
||||
|
||||
|
|
@ -18,11 +17,9 @@ 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),
|
||||
customerFilters: () => filters.customer(),
|
||||
},
|
||||
Mutation: {
|
||||
setCustomer: (root, { customerId, customerInput }, context) => {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function ticker(fiatCode, cryptoCode, tickerName) {
|
|||
return getCurrencyRates(ticker, fiatCode, cryptoCode)
|
||||
}
|
||||
|
||||
return getRate(RETRIES, tickerName, defaultFiatMarket(tickerName)).then(
|
||||
return getRate(RETRIES, fiatCode, defaultFiatMarket(tickerName)).then(
|
||||
({ fxRate }) => {
|
||||
try {
|
||||
return getCurrencyRates(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ const loadRoutes = async () => {
|
|||
app.use(compression({ threshold: 500 }))
|
||||
app.use(helmet())
|
||||
app.use(nocache())
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
app.use(express.json({ limit: '25mb' }))
|
||||
|
||||
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
||||
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
||||
|
|
|
|||
|
|
@ -311,7 +311,13 @@ function getExternalComplianceLink(req, res, next) {
|
|||
.then(url => respond(req, res, { url }))
|
||||
}
|
||||
|
||||
function addOrUpdateCustomer(customerData, deviceId, config, isEmailAuth) {
|
||||
function addOrUpdateCustomer(
|
||||
customerData,
|
||||
deviceId,
|
||||
config,
|
||||
isEmailAuth,
|
||||
cryptoCode,
|
||||
) {
|
||||
const triggers = configManager.getTriggers(config)
|
||||
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
|
||||
|
||||
|
|
@ -346,6 +352,18 @@ function addOrUpdateCustomer(customerData, deviceId, config, isEmailAuth) {
|
|||
.getCustomerActiveIndividualDiscount(customer.id)
|
||||
.then(discount => ({ ...customer, discount }))
|
||||
})
|
||||
.then(customer => {
|
||||
const enableLastUsedAddress = !!configManager.getWalletSettings(
|
||||
cryptoCode,
|
||||
config,
|
||||
).enableLastUsedAddress
|
||||
if (!cryptoCode || !enableLastUsedAddress) return customer
|
||||
return customers
|
||||
.getLastUsedAddress(customer.id, cryptoCode)
|
||||
.then(lastUsedAddress => {
|
||||
return { ...customer, lastUsedAddress }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getOrAddCustomerPhone(req, res, next) {
|
||||
|
|
@ -354,6 +372,7 @@ function getOrAddCustomerPhone(req, res, next) {
|
|||
|
||||
const pi = plugins(req.settings, deviceId)
|
||||
const phone = req.body.phone
|
||||
const cryptoCode = req.query.cryptoCode
|
||||
|
||||
return pi
|
||||
.getPhoneCode(phone)
|
||||
|
|
@ -363,6 +382,7 @@ function getOrAddCustomerPhone(req, res, next) {
|
|||
deviceId,
|
||||
req.settings.config,
|
||||
false,
|
||||
cryptoCode,
|
||||
).then(customer => respond(req, res, { code, customer }))
|
||||
})
|
||||
.catch(err => {
|
||||
|
|
@ -375,6 +395,7 @@ function getOrAddCustomerPhone(req, res, next) {
|
|||
function getOrAddCustomerEmail(req, res, next) {
|
||||
const deviceId = req.deviceId
|
||||
const customerData = req.body
|
||||
const cryptoCode = req.query.cryptoCode
|
||||
|
||||
const pi = plugins(req.settings, req.deviceId)
|
||||
const email = req.body.email
|
||||
|
|
@ -387,6 +408,7 @@ function getOrAddCustomerEmail(req, res, next) {
|
|||
deviceId,
|
||||
req.settings.config,
|
||||
true,
|
||||
cryptoCode,
|
||||
).then(customer => respond(req, res, { code, customer }))
|
||||
})
|
||||
.catch(err => {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,12 @@ function getTx(req, res, next) {
|
|||
return helpers
|
||||
.fetchStatusTx(req.params.id, req.query.status)
|
||||
.then(r => res.json(r))
|
||||
.catch(next)
|
||||
.catch(err => {
|
||||
if (err.name === 'HTTPError') {
|
||||
return res.status(err.code).send(err.message)
|
||||
}
|
||||
next(err)
|
||||
})
|
||||
}
|
||||
|
||||
return next(httpError('Not Found', 404))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const T = require('./time')
|
|||
// FP operations on Postgres result in very big errors.
|
||||
// E.g.: 1853.013808 * 1000 = 1866149.494
|
||||
const REDEEMABLE_AGE = T.day / 1000
|
||||
const MAX_THRESHOLD_DAYS = 365 * 50 // 50 years maximum
|
||||
|
||||
function process(tx, pi) {
|
||||
const mtx = massage(tx)
|
||||
|
|
@ -92,7 +93,9 @@ function customerHistory(customerId, thresholdDays) {
|
|||
AND fiat > 0
|
||||
) ch WHERE NOT ch.expired ORDER BY ch.created`
|
||||
|
||||
const days = _.isNil(thresholdDays) ? 0 : thresholdDays
|
||||
const days = _.isNil(thresholdDays)
|
||||
? 0
|
||||
: Math.min(thresholdDays, MAX_THRESHOLD_DAYS)
|
||||
return db.any(sql, [customerId, `${days} days`, '60 minutes', REDEEMABLE_AGE])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = next =>
|
||||
db.multi(
|
||||
[
|
||||
`CREATE INDEX cash_in_txs_customer_id_idx ON cash_in_txs (customer_id);`,
|
||||
`CREATE INDEX cash_out_txs_customer_id_idx ON cash_out_txs (customer_id);`,
|
||||
],
|
||||
next,
|
||||
)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
const { saveConfig } = require('../lib/new-settings-loader')
|
||||
|
||||
exports.up = function (next) {
|
||||
const newConfig = {
|
||||
wallets_advanced_enableLastUsedAddress: false,
|
||||
}
|
||||
return saveConfig(newConfig)
|
||||
.then(next)
|
||||
.catch(err => {
|
||||
return next(err)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -76,7 +76,6 @@
|
|||
"p-each-series": "^1.0.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"p-retry": "^4.4.0",
|
||||
"pg-native": "^3.0.0",
|
||||
"pg-promise": "^10.10.2",
|
||||
"pify": "^3.0.0",
|
||||
"pretty-ms": "^2.1.0",
|
||||
|
|
@ -89,6 +88,7 @@
|
|||
"telnyx": "^1.25.5",
|
||||
"tronweb": "^5.3.0",
|
||||
"twilio": "^3.6.1",
|
||||
"typesafe-db": "workspace:*",
|
||||
"uuid": "8.3.2",
|
||||
"web3": "1.7.1",
|
||||
"winston": "^2.4.2",
|
||||
|
|
@ -123,34 +123,16 @@
|
|||
"lamassu-eth-recovery": "./bin/lamassu-eth-recovery",
|
||||
"lamassu-trx-recovery": "./bin/lamassu-trx-recovery",
|
||||
"lamassu-update-cassettes": "./bin/lamassu-update-cassettes",
|
||||
"lamassu-clean-parsed-id": "./bin/lamassu-clean-parsed-id"
|
||||
"lamassu-clean-parsed-id": "./bin/lamassu-clean-parsed-id",
|
||||
"lamassu-batch-diagnostics": "./bin/lamassu-batch-diagnostics"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node bin/lamassu-server",
|
||||
"test": "mocha --recursive tests",
|
||||
"jtest": "jest --detectOpenHandles",
|
||||
"build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu",
|
||||
"server": "nodemon bin/lamassu-server --mockScoring --logLevel silly",
|
||||
"admin-server": "nodemon bin/lamassu-admin-server --dev --logLevel silly",
|
||||
"watch": "concurrently \"npm:server\" \"npm:admin-server\"",
|
||||
"dev": "concurrently \"npm:server\" \"npm:admin-server\"",
|
||||
"server": "node --watch bin/lamassu-server --mockScoring --logLevel silly",
|
||||
"admin-server": "node --watch bin/lamassu-admin-server --dev --logLevel silly",
|
||||
"stress-test": "cd tests/stress/ && node index.js 50 -v"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"new-lamassu-admin/*"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^5.3.0",
|
||||
"jest": "^26.6.3",
|
||||
"nodemon": "^2.0.6",
|
||||
"standard": "^12.0.1"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/lamassu-admin-elm",
|
||||
"/public",
|
||||
"/new-lamassu-admin"
|
||||
]
|
||||
"concurrently": "^5.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ setEnvVariable('KEY_PATH', `${process.env.PWD}/certs/Lamassu_OP.key`)
|
|||
|
||||
setEnvVariable(
|
||||
'MNEMONIC_PATH',
|
||||
`${process.env.HOME}/.lamassu/mnemonics/mnemonic.txt`,
|
||||
`${process.env.PWD}/.lamassu/mnemonics/mnemonic.txt`,
|
||||
)
|
||||
|
||||
setEnvVariable('BLOCKCHAIN_DIR', `${process.env.PWD}/blockchains`)
|
||||
setEnvVariable('OFAC_DATA_DIR', `${process.env.HOME}/.lamassu/ofac`)
|
||||
setEnvVariable('ID_PHOTO_CARD_DIR', `${process.env.HOME}/.lamassu/idphotocard`)
|
||||
setEnvVariable('FRONT_CAMERA_DIR', `${process.env.HOME}/.lamassu/frontcamera`)
|
||||
setEnvVariable('OPERATOR_DATA_DIR', `${process.env.HOME}/.lamassu/operatordata`)
|
||||
setEnvVariable('OFAC_DATA_DIR', `${process.env.PWD}/.lamassu/ofac`)
|
||||
setEnvVariable('ID_PHOTO_CARD_DIR', `${process.env.PWD}/.lamassu/idphotocard`)
|
||||
setEnvVariable('FRONT_CAMERA_DIR', `${process.env.PWD}/.lamassu/frontcamera`)
|
||||
setEnvVariable('OPERATOR_DATA_DIR', `${process.env.PWD}/.lamassu/operatordata`)
|
||||
|
||||
setEnvVariable('BTC_NODE_LOCATION', 'remote')
|
||||
setEnvVariable('BTC_WALLET_LOCATION', 'local')
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ set -e
|
|||
DOMAIN=localhost
|
||||
[ ! -z "$1" ] && DOMAIN=$1
|
||||
|
||||
CONFIG_DIR=$HOME/.lamassu
|
||||
SERVER_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
CONFIG_DIR=$SERVER_DIR/.lamassu
|
||||
LOG_FILE=/tmp/cert-gen.log
|
||||
CERT_DIR=$PWD/certs
|
||||
KEY_DIR=$PWD/certs
|
||||
LAMASSU_CA_PATH=$PWD/Lamassu_CA.pem
|
||||
CERT_DIR=$SERVER_DIR/certs
|
||||
KEY_DIR=$SERVER_DIR/certs
|
||||
LAMASSU_CA_PATH=$SERVER_DIR/Lamassu_CA.pem
|
||||
POSTGRES_PASS=postgres123
|
||||
OFAC_DATA_DIR=$CONFIG_DIR/ofac
|
||||
IDPHOTOCARD_DIR=$CONFIG_DIR/idphotocard
|
||||
|
|
@ -24,7 +26,7 @@ MNEMONIC_DIR=$CONFIG_DIR/mnemonics
|
|||
MNEMONIC_FILE=$MNEMONIC_DIR/mnemonic.txt
|
||||
mkdir -p $MNEMONIC_DIR >> $LOG_FILE 2>&1
|
||||
SEED=$(openssl rand -hex 32)
|
||||
MNEMONIC=$($PWD/bin/bip39 $SEED)
|
||||
MNEMONIC=$($SERVER_DIR/bin/bip39 $SEED)
|
||||
echo "$MNEMONIC" > $MNEMONIC_FILE
|
||||
|
||||
echo "Generating SSL certificates..."
|
||||
|
|
@ -90,6 +92,6 @@ rm /tmp/Lamassu_OP.csr.pem
|
|||
mkdir -p $OFAC_DATA_DIR/sources
|
||||
touch $OFAC_DATA_DIR/etags.json
|
||||
|
||||
node tools/build-dev-env.js
|
||||
(cd $SERVER_DIR && node tools/build-dev-env.js)
|
||||
|
||||
echo "Done."
|
||||
|
|
|
|||
28
packages/typesafe-db/package.json
Normal file
28
packages/typesafe-db/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "typesafe-db",
|
||||
"version": "11.0.0-beta.0",
|
||||
"license": "../LICENSE",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/pg": "^8.11.10",
|
||||
"kysely-codegen": "^0.18.5",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./lib/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"dev": "tsc --watch",
|
||||
"generate-types": "kysely-codegen --camel-case --out-file ./src/types/types.d.ts",
|
||||
"postinstall": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"kysely": "^0.28.2",
|
||||
"pg": "^8.16.0"
|
||||
}
|
||||
}
|
||||
184
packages/typesafe-db/src/customers.ts
Normal file
184
packages/typesafe-db/src/customers.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import db from './db.js'
|
||||
import { ExpressionBuilder } from 'kysely'
|
||||
import { Customers, DB, EditedCustomerData } from './types/types.js'
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres'
|
||||
|
||||
type CustomerEB = ExpressionBuilder<DB & { c: Customers }, 'c'>
|
||||
type CustomerWithEditedEB = ExpressionBuilder<
|
||||
DB & { c: Customers } & { e: EditedCustomerData | null },
|
||||
'c' | 'e'
|
||||
>
|
||||
|
||||
const ANON_ID = '47ac1184-8102-11e7-9079-8f13a7117867'
|
||||
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')
|
||||
}
|
||||
|
||||
function selectNewestIdCardData(eb: CustomerWithEditedEB, ref: any) {
|
||||
return eb
|
||||
.case()
|
||||
.when(
|
||||
eb.and([
|
||||
eb(ref('e.idCardDataAt'), 'is not', null),
|
||||
eb.or([
|
||||
eb(ref('c.idCardDataAt'), 'is', null),
|
||||
eb(ref('e.idCardDataAt'), '>', ref('c.idCardDataAt')),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.then(ref('e.idCardData'))
|
||||
.else(ref('c.idCardData'))
|
||||
.end()
|
||||
}
|
||||
|
||||
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')
|
||||
.leftJoin('editedCustomerData as e', 'e.customerId', 'c.id')
|
||||
.leftJoinLateral(joinTxsTotals, join => join.onTrue())
|
||||
.leftJoinLateral(joinLatestTx, join => join.onTrue())
|
||||
.select(({ eb, fn, val, ref }) => [
|
||||
'c.id',
|
||||
'c.phone',
|
||||
'c.authorizedOverride',
|
||||
'c.frontCameraPath',
|
||||
'c.frontCameraOverride',
|
||||
'c.idCardPhotoPath',
|
||||
'c.idCardPhotoOverride',
|
||||
selectNewestIdCardData(eb, ref).as('idCardData'),
|
||||
'c.idCardDataOverride',
|
||||
'c.email',
|
||||
'c.usSsn',
|
||||
'c.usSsnOverride',
|
||||
'c.sanctions',
|
||||
'c.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'),
|
||||
])
|
||||
.where('c.id', '!=', ANON_ID)
|
||||
.$if(options.withCustomInfoRequest, qb =>
|
||||
qb.select(({ eb, ref }) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('customersCustomInfoRequests')
|
||||
.selectAll()
|
||||
.where('customerId', '=', ref('c.id')),
|
||||
).as('customInfoRequestData'),
|
||||
),
|
||||
)
|
||||
.orderBy('lastActive', 'desc')
|
||||
.execute()
|
||||
}
|
||||
|
||||
export { getCustomerList }
|
||||
23
packages/typesafe-db/src/db.ts
Normal file
23
packages/typesafe-db/src/db.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { DB } from './types/types.js'
|
||||
import { Pool } from 'pg'
|
||||
import { Kysely, PostgresDialect, CamelCasePlugin } from 'kysely'
|
||||
|
||||
const POSTGRES_USER = process.env.POSTGRES_USER
|
||||
const POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD
|
||||
const POSTGRES_HOST = process.env.POSTGRES_HOST
|
||||
const POSTGRES_PORT = process.env.POSTGRES_PORT
|
||||
const POSTGRES_DB = process.env.POSTGRES_DB
|
||||
|
||||
const PSQL_URL = `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}`
|
||||
|
||||
const dialect = new PostgresDialect({
|
||||
pool: new Pool({
|
||||
connectionString: PSQL_URL,
|
||||
max: 5,
|
||||
}),
|
||||
})
|
||||
|
||||
export default new Kysely<DB>({
|
||||
dialect,
|
||||
plugins: [new CamelCasePlugin()],
|
||||
})
|
||||
1
packages/typesafe-db/src/index.ts
Normal file
1
packages/typesafe-db/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * as customers from './customers.js'
|
||||
745
packages/typesafe-db/src/types/types.d.ts
vendored
Normal file
745
packages/typesafe-db/src/types/types.d.ts
vendored
Normal 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue