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
|
.git
|
||||||
.direnv
|
.direnv
|
||||||
.envrc
|
.envrc
|
||||||
58
.github/workflows/build.yml
vendored
58
.github/workflows/build.yml
vendored
|
|
@ -4,42 +4,48 @@ on: [ workflow_dispatch ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
everything:
|
everything:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Turbo cache
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: .turbo
|
||||||
key: ${{ runner.os }}-buildx-updatetar
|
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-buildx-updatetar
|
${{ runner.os }}-turbo-
|
||||||
- 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
|
|
||||||
|
|
||||||
- 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: |
|
run: |
|
||||||
docker create --name extract_artifact ci_image:latest
|
# Create production-ready server package using pnpm deploy
|
||||||
docker cp extract_artifact:/lamassu-server.tar.gz ./lamassu-server.tar.gz
|
pnpm deploy --filter=./packages/server --prod lamassu-server --legacy
|
||||||
docker rm extract_artifact
|
|
||||||
|
# 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
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: lamassu-server.tar.gz
|
name: lamassu-server.tar.gz
|
||||||
path: 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:
|
jobs:
|
||||||
build-and-publish:
|
build-and-publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Setup Turbo cache
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: .turbo
|
||||||
key: ${{ runner.os }}-buildx-updatetar
|
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||||
restore-keys: |
|
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
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|
@ -34,29 +69,24 @@ jobs:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push server image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: lamassu-server
|
||||||
push: true
|
push: true
|
||||||
target: l-s
|
target: l-s
|
||||||
file: ./build/server.Dockerfile
|
file: lamassu-server/server.Dockerfile
|
||||||
tags: ${{ env.DOCKERHUB_SERVER_REPO }}:latest
|
tags: ${{ env.DOCKERHUB_SERVER_REPO }}:latest
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=gha
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push admin server image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: lamassu-server
|
||||||
push: true
|
push: true
|
||||||
target: l-a-s
|
target: l-a-s
|
||||||
file: ./build/server.Dockerfile
|
file: lamassu-server/server.Dockerfile
|
||||||
tags: ${{ env.DOCKERHUB_ADMIN_REPO }}:latest
|
tags: ${{ env.DOCKERHUB_ADMIN_REPO }}:latest
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=gha
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
- name: Move cache
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/.buildx-cache
|
|
||||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
|
||||||
|
|
|
||||||
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
|
**/node_modules
|
||||||
**/.env
|
**/.env
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.settings/
|
.settings/
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
packages/server/.lamassu
|
||||||
packages/server/certs/
|
packages/server/certs/
|
||||||
packages/server/tests/stress/machines
|
packages/server/tests/stress/machines
|
||||||
packages/server/tests/stress/config.json
|
packages/server/tests/stress/config.json
|
||||||
|
packages/typesafe-db/lib/
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
# Run linting
|
||||||
npx lint-staged
|
npx lint-staged
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
nodejs 22
|
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.
|
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)
|
- Nodejs 22
|
||||||
See [lamassu-remote-install/README.md](lamassu-remote-install/README.md).
|
- 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
|
```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:
|
services:
|
||||||
lamassu-server:
|
lamassu-server:
|
||||||
build:
|
image: lamassu/lamassu-server:latest
|
||||||
context: .
|
|
||||||
dockerfile: build/server.Dockerfile
|
|
||||||
target: l-s
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
network_mode: host
|
||||||
- 3000:3000
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./lamassu-data:/lamassu-data
|
- ./lamassu-data:/lamassu-data
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- POSTGRES_USER=postgres
|
- POSTGRES_USER=postgres
|
||||||
- POSTGRES_PASSWORD=postgres123
|
- POSTGRES_PASSWORD=postgres123
|
||||||
- POSTGRES_HOST=host.docker.internal
|
- POSTGRES_HOST=localhost
|
||||||
- POSTGRES_PORT=5432
|
- POSTGRES_PORT=5432
|
||||||
- POSTGRES_DB=lamassu
|
- POSTGRES_DB=lamassu
|
||||||
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
||||||
|
|
@ -31,20 +27,16 @@ services:
|
||||||
- LOG_LEVEL=info
|
- LOG_LEVEL=info
|
||||||
|
|
||||||
lamassu-admin-server:
|
lamassu-admin-server:
|
||||||
build:
|
image: lamassu/lamassu-admin-server:latest
|
||||||
context: .
|
|
||||||
dockerfile: build/server.Dockerfile
|
|
||||||
target: l-a-s
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
ports:
|
network_mode: host
|
||||||
- 443:443
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./lamassu-data:/lamassu-data
|
- ./lamassu-data:/lamassu-data
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- POSTGRES_USER=postgres
|
- POSTGRES_USER=postgres
|
||||||
- POSTGRES_PASSWORD=postgres123
|
- POSTGRES_PASSWORD=postgres123
|
||||||
- POSTGRES_HOST=host.docker.internal
|
- POSTGRES_HOST=localhost
|
||||||
- POSTGRES_PORT=5432
|
- POSTGRES_PORT=5432
|
||||||
- POSTGRES_DB=lamassu
|
- POSTGRES_DB=lamassu
|
||||||
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
- CA_PATH=/lamassu-data/certs/Lamassu_OP_Root_CA.pem
|
||||||
|
|
@ -1,49 +1,17 @@
|
||||||
FROM node:22-alpine AS build
|
FROM node:22-bullseye AS base
|
||||||
RUN apk add --no-cache npm git curl build-base net-tools python3 postgresql-dev
|
RUN apt install openssl ca-certificates
|
||||||
|
|
||||||
WORKDIR /lamassu-server
|
WORKDIR /lamassu-server
|
||||||
|
|
||||||
COPY ["packages/server/package.json", "package-lock.json", "./"]
|
# Copy the pre-built production package from CI (with node_modules)
|
||||||
RUN npm version --allow-same-version --git-tag-version false --commit-hooks false 1.0.0
|
COPY . ./
|
||||||
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
|
|
||||||
|
|
||||||
|
FROM base AS l-s
|
||||||
RUN chmod +x /lamassu-server/bin/lamassu-server-entrypoint.sh
|
RUN chmod +x /lamassu-server/bin/lamassu-server-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ]
|
ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ]
|
||||||
|
|
||||||
|
FROM base AS l-a-s
|
||||||
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
|
|
||||||
|
|
||||||
RUN chmod +x /lamassu-server/bin/lamassu-admin-server-entrypoint.sh
|
RUN chmod +x /lamassu-server/bin/lamassu-admin-server-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
|
|
||||||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-admin-server-entrypoint.sh" ]
|
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 { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
import eslintConfigPrettier from 'eslint-config-prettier/flat'
|
||||||
import pluginJest from 'eslint-plugin-jest'
|
import vitest from 'eslint-plugin-vitest'
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
|
'**/.lamassu',
|
||||||
'**/build',
|
'**/build',
|
||||||
'**/package.json',
|
'**/package.json',
|
||||||
'**/package-lock.json',
|
'**/package-lock.json',
|
||||||
|
|
@ -59,16 +60,12 @@ export default defineConfig([
|
||||||
{
|
{
|
||||||
// update this to match your test files
|
// update this to match your test files
|
||||||
files: ['**/*.spec.js', '**/*.test.js'],
|
files: ['**/*.spec.js', '**/*.test.js'],
|
||||||
plugins: { jest: pluginJest },
|
plugins: {
|
||||||
languageOptions: {
|
vitest,
|
||||||
globals: pluginJest.environments.globals.globals,
|
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'jest/no-disabled-tests': 'warn',
|
...vitest.configs.recommended.rules, // you can also use vitest.configs.all.rules to enable all rules
|
||||||
'jest/no-focused-tests': 'error',
|
'vitest/max-nested-describe': ['error', { max: 3 }], // you can also modify rules' behavior using option like this
|
||||||
'jest/no-identical-title': 'error',
|
|
||||||
'jest/prefer-to-have-length': 'warn',
|
|
||||||
'jest/valid-expect': 'error',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
|
||||||
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",
|
"version": "11.0.0-beta.1",
|
||||||
"license": "./LICENSE",
|
"license": "./LICENSE",
|
||||||
"author": "Lamassu (https://lamassu.is)",
|
"author": "Lamassu (https://lamassu.is)",
|
||||||
|
"packageManager": "pnpm@10.11.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/lamassu/lamassu-server.git"
|
"url": "https://github.com/lamassu/lamassu-server.git"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"engines": {
|
||||||
"packages/server",
|
"node": ">=22.0.0"
|
||||||
"packages/admin-ui"
|
},
|
||||||
],
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/css": "^0.7.0",
|
"@eslint/css": "^0.7.0",
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.26.0",
|
||||||
"@eslint/json": "^0.12.0",
|
"@eslint/json": "^0.12.0",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
"eslint-plugin-jest": "^28.11.0",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
|
"eslint-plugin-vitest": "^0.5.4",
|
||||||
"globals": "^16.1.0",
|
"globals": "^16.1.0",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"lint-staged": "^16.0.0",
|
"lint-staged": "^16.0.0",
|
||||||
"prettier": "^3.5.3"
|
"prettier": "^3.5.3",
|
||||||
|
"turbo": "^2.5.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install"
|
"prepare": "husky install",
|
||||||
|
"build": "turbo build",
|
||||||
|
"dev": "turbo dev",
|
||||||
|
"test": "turbo test"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
|
|
|
||||||
|
|
@ -10,31 +10,27 @@ To take advantage of that make sure to run `git commit` from within this folder.
|
||||||
|
|
||||||
## Available Scripts
|
## 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>
|
In the admin-ui package directory, you can run:
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
|
||||||
|
|
||||||
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.
|
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>
|
Builds the app for production to the `build` folder.
|
||||||
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>
|
|
||||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
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",
|
"@lamassu/coins": "v1.6.1",
|
||||||
"@mui/icons-material": "^7.1.0",
|
"@mui/icons-material": "^7.1.0",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
|
"@mui/x-date-pickers": "^8.3.1",
|
||||||
"@simplewebauthn/browser": "^3.0.0",
|
"@simplewebauthn/browser": "^3.0.0",
|
||||||
"apollo-upload-client": "^18.0.0",
|
"apollo-upload-client": "^18.0.0",
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
|
|
@ -25,9 +26,10 @@
|
||||||
"jszip": "^3.6.0",
|
"jszip": "^3.6.0",
|
||||||
"libphonenumber-js": "^1.11.15",
|
"libphonenumber-js": "^1.11.15",
|
||||||
"match-sorter": "^4.2.0",
|
"match-sorter": "^4.2.0",
|
||||||
|
"material-react-table": "^3.2.1",
|
||||||
"pretty-ms": "^2.1.0",
|
"pretty-ms": "^2.1.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"ramda": "^0.26.1",
|
"ramda": "^0.30.1",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-copy-to-clipboard": "^5.0.2",
|
"react-copy-to-clipboard": "^5.0.2",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
|
|
@ -49,12 +51,15 @@
|
||||||
"prettier": "3.4.1",
|
"prettier": "3.4.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"vite": "^6.0.1",
|
"vite": "^6.0.1",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
|
"vitest": "^3.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Router } from 'wouter'
|
import { Router } from 'wouter'
|
||||||
import ApolloProvider from './utils/apollo'
|
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 AppContext from './AppContext'
|
||||||
import theme from './styling/theme'
|
import theme from './styling/theme'
|
||||||
|
|
@ -33,16 +35,18 @@ const App = () => {
|
||||||
isDirtyForm,
|
isDirtyForm,
|
||||||
setDirtyForm,
|
setDirtyForm,
|
||||||
}}>
|
}}>
|
||||||
<Router hook={useLocationWithConfirmation}>
|
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||||
<ApolloProvider>
|
<Router hook={useLocationWithConfirmation}>
|
||||||
<StyledEngineProvider enableCssLayer>
|
<ApolloProvider>
|
||||||
<ThemeProvider theme={theme}>
|
<StyledEngineProvider enableCssLayer>
|
||||||
<CssBaseline />
|
<ThemeProvider theme={theme}>
|
||||||
<Main />
|
<CssBaseline />
|
||||||
</ThemeProvider>
|
<Main />
|
||||||
</StyledEngineProvider>
|
</ThemeProvider>
|
||||||
</ApolloProvider>
|
</StyledEngineProvider>
|
||||||
</Router>
|
</ApolloProvider>
|
||||||
|
</Router>
|
||||||
|
</LocalizationProvider>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import Paper from '@mui/material/Paper'
|
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 default CollapsibleCard
|
||||||
export { cardState }
|
export { cardState }
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ const NotificationCenter = ({
|
||||||
const notificationsToShow =
|
const notificationsToShow =
|
||||||
!showingUnread || !hasUnread
|
!showingUnread || !hasUnread
|
||||||
? notifications
|
? notifications
|
||||||
: R.filter(R.propEq('read', false))(notifications)
|
: R.filter(R.propEq(false, 'read'))(notifications)
|
||||||
return notificationsToShow.map(n => {
|
return notificationsToShow.map(n => {
|
||||||
return (
|
return (
|
||||||
<NotificationRow
|
<NotificationRow
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupStriped = elements => {
|
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) {
|
if (!toStripe.length) {
|
||||||
return elements
|
return elements
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ const ETable = ({
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
|
||||||
const it = validationSchema.cast(value, { assert: 'ignore-optionality' })
|
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)
|
const list = index !== -1 ? R.update(index, it, data) : R.prepend(it, data)
|
||||||
|
|
||||||
if (!R.equals(data[index], it)) {
|
if (!R.equals(data[index], it)) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const Autocomplete = ({
|
||||||
autoFocus,
|
autoFocus,
|
||||||
...props
|
...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 mapToValue = R.prop(valueProp)
|
||||||
|
|
||||||
const getValue = () => {
|
const getValue = () => {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const ToggleButtonGroup = ({
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<MUIToggleButtonGroup
|
<MUIToggleButtonGroup
|
||||||
|
className="flex flex-col gap-4"
|
||||||
size={size}
|
size={size}
|
||||||
name={name}
|
name={name}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,9 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*TODO important because of tailwind integration with MUI*/
|
||||||
.fullPartP {
|
.fullPartP {
|
||||||
color: white;
|
color: white !important;
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useLazyQuery, useQuery, gql } from '@apollo/client'
|
import { useLazyQuery, useQuery, gql } from '@apollo/client'
|
||||||
import { subMinutes } from 'date-fns'
|
import { subMinutes } from 'date-fns'
|
||||||
import FileSaver from 'file-saver'
|
import FileSaver from 'file-saver'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
import Modal from '../Modal'
|
import Modal from '../Modal'
|
||||||
import { H3, P } from '../typography'
|
import { H3, P } from '../typography'
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ const createCsv = async ({ machineLogsCsv }) => {
|
||||||
const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
||||||
const [state, setState] = useState(STATES.INITIAL)
|
const [state, setState] = useState(STATES.INITIAL)
|
||||||
const [timestamp, setTimestamp] = useState(null)
|
const [timestamp, setTimestamp] = useState(null)
|
||||||
let timeout = null
|
const timeoutRef = useRef(null)
|
||||||
|
|
||||||
const [fetchSummary, { loading }] = useLazyQuery(MACHINE_LOGS, {
|
const [fetchSummary, { loading }] = useLazyQuery(MACHINE_LOGS, {
|
||||||
onCompleted: data => createCsv(data),
|
onCompleted: data => createCsv(data),
|
||||||
|
|
@ -76,24 +76,41 @@ const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
||||||
data.machine.diagnostics.timestamp &&
|
data.machine.diagnostics.timestamp &&
|
||||||
data.machine.diagnostics.timestamp !== timestamp
|
data.machine.diagnostics.timestamp !== timestamp
|
||||||
) {
|
) {
|
||||||
clearTimeout(timeout)
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = null
|
||||||
|
}
|
||||||
setTimestamp(data.machine.diagnostics.timestamp)
|
setTimestamp(data.machine.diagnostics.timestamp)
|
||||||
setState(STATES.FILLED)
|
setState(STATES.FILLED)
|
||||||
stopPolling()
|
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}/`
|
const path = `/operator-data/diagnostics/${deviceId}/`
|
||||||
|
|
||||||
function runDiagnostics() {
|
const runDiagnostics = () => {
|
||||||
|
setState(STATES.RUNNING)
|
||||||
startPolling(2000)
|
startPolling(2000)
|
||||||
|
|
||||||
timeout = setTimeout(() => {
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
setState(STATES.FAILURE)
|
setState(STATES.FAILURE)
|
||||||
stopPolling()
|
stopPolling()
|
||||||
|
timeoutRef.current = null
|
||||||
}, 60 * 1000)
|
}, 60 * 1000)
|
||||||
|
|
||||||
setState(STATES.RUNNING)
|
|
||||||
sendAction()
|
sendAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +157,7 @@ const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
||||||
<H3>Scan</H3>
|
<H3>Scan</H3>
|
||||||
<img
|
<img
|
||||||
className="w-88"
|
className="w-88"
|
||||||
src={path + 'scan.jpg'}
|
src={`${path}scan.jpg?${Date.now()}`}
|
||||||
alt="Failure getting photo"
|
alt="Failure getting photo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -148,7 +165,7 @@ const DiagnosticsModal = ({ onClose, deviceId, sendAction }) => {
|
||||||
<H3>Front</H3>
|
<H3>Front</H3>
|
||||||
<img
|
<img
|
||||||
className="w-88"
|
className="w-88"
|
||||||
src={path + 'front.jpg'}
|
src={`${path}front.jpg?${Date.now()}`}
|
||||||
alt="Failure getting photo"
|
alt="Failure getting photo"
|
||||||
/>
|
/>
|
||||||
<P></P>
|
<P></P>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import React, { memo } from 'react'
|
||||||
const TableRow = memo(
|
const TableRow = memo(
|
||||||
({ className, children, header, error, success, size = 'sm', ...props }) => {
|
({ className, children, header, error, success, size = 'sm', ...props }) => {
|
||||||
const classnamesObj = {
|
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-8': !header && size === 'sm',
|
||||||
'h-9 font-bold text-base ': !header && size === 'lg',
|
'h-9 font-bold text-base ': !header && size === 'lg',
|
||||||
'bg-misty-rose': error,
|
'bg-misty-rose': error,
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ const Analytics = () => {
|
||||||
|
|
||||||
const convertFiatToLocale = item => {
|
const convertFiatToLocale = item => {
|
||||||
if (item.fiatCode === fiatLocale) return 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 localeRate = R.find(R.propEq('code', fiatLocale))(rates)
|
||||||
const multiplier = localeRate?.rate / itemRate?.rate
|
const multiplier = localeRate?.rate / itemRate?.rate
|
||||||
return { ...item, fiat: parseFloat(item.fiat) * multiplier }
|
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" />}
|
{R.isEmpty(machines) && <EmptyTable message="No machines so far" />}
|
||||||
{wizard && (
|
{wizard && (
|
||||||
<Wizard
|
<Wizard
|
||||||
machine={R.find(R.propEq('deviceId', wizard))(machines)}
|
machine={R.find(R.propEq(wizard, 'deviceId'))(machines)}
|
||||||
onClose={() => setWizard(false)}
|
onClose={() => setWizard(false)}
|
||||||
save={save}
|
save={save}
|
||||||
error={error?.message}
|
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({
|
setState({
|
||||||
step: step + 1,
|
step: step + 1,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const CommissionsDetails = memo(
|
||||||
initialValues={commission}
|
initialValues={commission}
|
||||||
save={save}
|
save={save}
|
||||||
validationSchema={getSchema(locale)}
|
validationSchema={getSchema(locale)}
|
||||||
data={R.of(commission)}
|
data={R.of(Array, commission)}
|
||||||
elements={mainFields(currency)}
|
elements={mainFields(currency)}
|
||||||
setEditing={onEditingDefault}
|
setEditing={onEditingDefault}
|
||||||
forceDisable={isEditingOverrides}
|
forceDisable={isEditingOverrides}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const getElement = (code, display) => ({
|
||||||
const sortCommissionsBy = prop => {
|
const sortCommissionsBy = prop => {
|
||||||
switch (prop) {
|
switch (prop) {
|
||||||
case ORDER_OPTIONS[0]:
|
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]:
|
case ORDER_OPTIONS[1]:
|
||||||
return R.sortBy(R.path(['cryptoCurrencies', 0]))
|
return R.sortBy(R.path(['cryptoCurrencies', 0]))
|
||||||
default:
|
default:
|
||||||
|
|
@ -80,7 +80,7 @@ const CommissionsList = memo(
|
||||||
|
|
||||||
const getMachineCoins = deviceId => {
|
const getMachineCoins = deviceId => {
|
||||||
const override = R.prop('overrides', localeConfig)?.find(
|
const override = R.prop('overrides', localeConfig)?.find(
|
||||||
R.propEq('machine', deviceId),
|
R.propEq(deviceId, 'machine'),
|
||||||
)
|
)
|
||||||
|
|
||||||
const machineCoins = override
|
const machineCoins = override
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ const getView = (data, code, compare) => it => {
|
||||||
if (!data) return ''
|
if (!data) return ''
|
||||||
|
|
||||||
// The following boolean should come undefined if it is rendering an unpaired machine
|
// 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'
|
return attribute ? R.prop(code, attribute) : 'Unpaired machine'
|
||||||
}
|
}
|
||||||
|
|
@ -294,8 +294,8 @@ const getAlreadyUsed = (id, machine, values) => {
|
||||||
const getCrypto = R.prop('cryptoCurrencies')
|
const getCrypto = R.prop('cryptoCurrencies')
|
||||||
const getMachineId = R.prop('machine')
|
const getMachineId = R.prop('machine')
|
||||||
|
|
||||||
const filteredOverrides = R.filter(R.propEq('machine', machine))(values)
|
const filteredOverrides = R.filter(R.propEq(machine, 'machine'))(values)
|
||||||
const originalValue = R.find(R.propEq('id', id))(values)
|
const originalValue = R.find(R.propEq(id, 'id'))(values)
|
||||||
|
|
||||||
const originalCryptos = getCrypto(originalValue)
|
const originalCryptos = getCrypto(originalValue)
|
||||||
const originalMachineId = getMachineId(originalValue)
|
const originalMachineId = getMachineId(originalValue)
|
||||||
|
|
@ -407,7 +407,7 @@ const overridesDefaults = {
|
||||||
|
|
||||||
const getOrder = ({ machine, cryptoCurrencies }) => {
|
const getOrder = ({ machine, cryptoCurrencies }) => {
|
||||||
const isAllMachines = machine === ALL_MACHINES.deviceId
|
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 && isAllCoins) return 0
|
||||||
if (isAllMachines) return 1
|
if (isAllMachines) return 1
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ const CustomerData = ({
|
||||||
deleteEditedData: () => deleteEditedData({ idCardData: null }),
|
deleteEditedData: () => deleteEditedData({ idCardData: null }),
|
||||||
save: values =>
|
save: values =>
|
||||||
editCustomer({
|
editCustomer({
|
||||||
idCardData: R.merge(idData, formatDates(values)),
|
idCardData: R.mergeRight(idData, formatDates(values)),
|
||||||
}),
|
}),
|
||||||
validationSchema: customerDataSchemas.idCardData,
|
validationSchema: customerDataSchemas.idCardData,
|
||||||
checkAgainstSanctions: () =>
|
checkAgainstSanctions: () =>
|
||||||
|
|
@ -167,7 +167,7 @@ const CustomerData = ({
|
||||||
save: values => {
|
save: values => {
|
||||||
editCustomer({
|
editCustomer({
|
||||||
subscriberInfo: {
|
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 * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useLocation } from 'wouter'
|
import { useLocation } from 'wouter'
|
||||||
import SearchBox from '../../components/SearchBox'
|
|
||||||
import SearchFilter from '../../components/SearchFilter'
|
|
||||||
import TitleSection from '../../components/layout/TitleSection'
|
import TitleSection from '../../components/layout/TitleSection'
|
||||||
import TxInIcon from '../../styling/icons/direction/cash-in.svg?react'
|
import TxInIcon from '../../styling/icons/direction/cash-in.svg?react'
|
||||||
import TxOutIcon from '../../styling/icons/direction/cash-out.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 CreateCustomerModal from './components/CreateCustomerModal'
|
||||||
import { getAuthorizedStatus } from './helper'
|
import { getAuthorizedStatus } from './helper'
|
||||||
|
|
||||||
const GET_CUSTOMER_FILTERS = gql`
|
|
||||||
query filters {
|
|
||||||
customerFilters {
|
|
||||||
type
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const GET_CUSTOMERS = gql`
|
const GET_CUSTOMERS = gql`
|
||||||
query configAndCustomers(
|
query configAndCustomers(
|
||||||
$phone: String
|
$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 Customers = () => {
|
||||||
const [, navigate] = useLocation()
|
const [, navigate] = useLocation()
|
||||||
|
|
||||||
const handleCustomerClicked = customer =>
|
const handleCustomerClicked = customer =>
|
||||||
navigate(`/compliance/customer/${customer.id}`)
|
navigate(`/compliance/customer/${customer.id}`)
|
||||||
|
|
||||||
const [filteredCustomers, setFilteredCustomers] = useState([])
|
const [customers, setCustomers] = useState([])
|
||||||
const [variables, setVariables] = useState({})
|
|
||||||
const [filters, setFilters] = useState([])
|
|
||||||
const [showCreationModal, setShowCreationModal] = useState(false)
|
const [showCreationModal, setShowCreationModal] = useState(false)
|
||||||
|
|
||||||
const {
|
const { data: customersResponse, loading: customerLoading } = useQuery(
|
||||||
data: customersResponse,
|
GET_CUSTOMERS,
|
||||||
loading: customerLoading,
|
{
|
||||||
refetch,
|
onCompleted: data => setCustomers(R.path(['customers'])(data)),
|
||||||
} = useQuery(GET_CUSTOMERS, {
|
},
|
||||||
variables,
|
)
|
||||||
onCompleted: data => setFilteredCustomers(R.path(['customers'])(data)),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: filtersResponse, loading: loadingFilters } =
|
|
||||||
useQuery(GET_CUSTOMER_FILTERS)
|
|
||||||
|
|
||||||
const [createNewCustomer] = useMutation(CREATE_CUSTOMER, {
|
const [createNewCustomer] = useMutation(CREATE_CUSTOMER, {
|
||||||
onCompleted: () => setShowCreationModal(false),
|
onCompleted: () => setShowCreationModal(false),
|
||||||
refetchQueries: () => [
|
refetchQueries: () => [
|
||||||
{
|
{
|
||||||
query: GET_CUSTOMERS,
|
query: GET_CUSTOMERS,
|
||||||
variables,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
@ -145,76 +123,12 @@ const Customers = () => {
|
||||||
const customersData = R.pipe(
|
const customersData = R.pipe(
|
||||||
R.map(setAuthorizedStatus),
|
R.map(setAuthorizedStatus),
|
||||||
R.sortWith([R.ascend(byAuthorized), R.descend(byLastActive)]),
|
R.sortWith([R.ascend(byAuthorized), R.descend(byLastActive)]),
|
||||||
)(filteredCustomers ?? [])
|
)(customers ?? [])
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TitleSection
|
<TitleSection
|
||||||
title="Customers"
|
title="Customers"
|
||||||
appendix={
|
|
||||||
<div className="flex ml-4">
|
|
||||||
<SearchBox
|
|
||||||
loading={loadingFilters}
|
|
||||||
filters={filters}
|
|
||||||
options={filterOptions}
|
|
||||||
inputPlaceholder={'Search customers'}
|
|
||||||
onChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
appendixRight={
|
appendixRight={
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Link color="primary" onClick={() => setShowCreationModal(true)}>
|
<Link color="primary" onClick={() => setShowCreationModal(true)}>
|
||||||
|
|
@ -227,21 +141,11 @@ const Customers = () => {
|
||||||
{ label: 'Cash-out', icon: <TxOutIcon /> },
|
{ label: 'Cash-out', icon: <TxOutIcon /> },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{filters.length > 0 && (
|
|
||||||
<SearchFilter
|
|
||||||
entries={customersData.length}
|
|
||||||
filters={filters}
|
|
||||||
onFilterDelete={onFilterDelete}
|
|
||||||
deleteAllFilters={deleteAllFilters}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<CustomersList
|
<CustomersList
|
||||||
data={customersData}
|
data={customersData}
|
||||||
locale={locale}
|
country={locale?.country}
|
||||||
onClick={handleCustomerClicked}
|
onClick={handleCustomerClicked}
|
||||||
loading={customerLoading}
|
loading={customerLoading}
|
||||||
triggers={triggers}
|
|
||||||
customRequests={customRequirementsData}
|
|
||||||
/>
|
/>
|
||||||
<CreateCustomerModal
|
<CreateCustomerModal
|
||||||
showModal={showCreationModal}
|
showModal={showCreationModal}
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,141 @@
|
||||||
|
import Visibility from '@mui/icons-material/Visibility'
|
||||||
import { format } from 'date-fns/fp'
|
import { format } from 'date-fns/fp'
|
||||||
import * as R from 'ramda'
|
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 { MainStatus } from '../../components/Status'
|
||||||
import DataTable from '../../components/tables/DataTable'
|
|
||||||
import TxInIcon from '../../styling/icons/direction/cash-in.svg?react'
|
import TxInIcon from '../../styling/icons/direction/cash-in.svg?react'
|
||||||
import TxOutIcon from '../../styling/icons/direction/cash-out.svg?react'
|
import TxOutIcon from '../../styling/icons/direction/cash-out.svg?react'
|
||||||
|
import {
|
||||||
|
defaultMaterialTableOpts,
|
||||||
|
alignRight,
|
||||||
|
} from '../../utils/materialReactTableOpts'
|
||||||
|
|
||||||
import { getFormattedPhone, getName } from './helper'
|
import { getFormattedPhone, getName } from './helper'
|
||||||
|
|
||||||
const CustomersList = ({ data, locale, onClick, loading }) => {
|
const CustomersList = ({ data, country, onClick, loading }) => {
|
||||||
const elements = [
|
const columns = useMemo(
|
||||||
{
|
() => [
|
||||||
header: 'Phone/email',
|
{
|
||||||
width: 199,
|
accessorKey: 'id',
|
||||||
view: it => `${getFormattedPhone(it.phone, locale.country) || ''}
|
header: 'ID',
|
||||||
${it.email || ''}`,
|
size: 315,
|
||||||
},
|
|
||||||
{
|
|
||||||
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}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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'] },
|
||||||
},
|
},
|
||||||
{
|
state: { isLoading: loading },
|
||||||
header: 'Status',
|
getRowId: it => it.id,
|
||||||
width: 191,
|
enableRowActions: true,
|
||||||
view: it => <MainStatus statuses={[it.authorizedStatus]} />,
|
renderRowActionMenuItems: ({ row }) => [
|
||||||
},
|
<MRT_ActionMenuItem //or just use a normal MUI MenuItem component
|
||||||
]
|
icon={<Visibility />}
|
||||||
|
key="view"
|
||||||
|
label="View"
|
||||||
|
onClick={() => onClick(row)}
|
||||||
|
table={table}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DataTable
|
<MaterialReactTable table={table} />
|
||||||
loading={loading}
|
|
||||||
emptyText="No customers so far"
|
|
||||||
elements={elements}
|
|
||||||
data={data}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,43 +45,28 @@ const getAuthorizedStatus = (it, triggers, customRequests) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingFieldStatus = R.map(ite => {
|
const getFieldsByStatus = status =>
|
||||||
if (isManualField(ite)) {
|
R.map(ite => {
|
||||||
if (uuidValidate(ite)) {
|
if (isManualField(ite)) {
|
||||||
const request = R.find(
|
if (uuidValidate(ite)) {
|
||||||
iter => iter.infoRequestId === ite,
|
const request = R.find(
|
||||||
it.customInfoRequests,
|
iter => iter.infoRequestId === ite,
|
||||||
)
|
it.customInfoRequests,
|
||||||
return !R.isNil(request) && R.equals(request.override, 'automatic')
|
)
|
||||||
|
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)
|
const pendingFieldStatus = getFieldsByStatus('automatic')
|
||||||
? it[`${ite}Path`]
|
const rejectedFieldStatus = getFieldsByStatus('blocked')
|
||||||
: 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)
|
|
||||||
|
|
||||||
if (it.authorizedOverride === CUSTOMER_BLOCKED)
|
if (it.authorizedOverride === CUSTOMER_BLOCKED)
|
||||||
return { label: 'Blocked', type: 'error' }
|
return { label: 'Blocked', type: 'error' }
|
||||||
|
|
@ -235,7 +220,7 @@ const ManualDataEntry = ({ selectedValues, customInfoRequirementOptions }) => {
|
||||||
: requirementOptions
|
: requirementOptions
|
||||||
|
|
||||||
const requirementName = displayRequirements
|
const requirementName = displayRequirements
|
||||||
? R.find(R.propEq('code', requirementSelected))(updatedRequirementOptions)
|
? R.find(R.propEq(requirementSelected, 'code'))(updatedRequirementOptions)
|
||||||
.display
|
.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 { useQuery, gql } from '@apollo/client'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Grid from '@mui/material/Grid'
|
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
@ -56,11 +55,9 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
||||||
</Label1>
|
</Label1>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Grid
|
<div
|
||||||
className={classnames({ 'm-0': true, 'max-h-115': showAllItems })}
|
className={classnames({ 'm-0 mt-2': true, 'max-h-115': showAllItems })}>
|
||||||
container
|
<div className="w-full flex-1">
|
||||||
spacing={1}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
{!alerts.length && (
|
{!alerts.length && (
|
||||||
<Label1 className="text-comet -ml-1 h-30">
|
<Label1 className="text-comet -ml-1 h-30">
|
||||||
No new alerts. Your system is running smoothly.
|
No new alerts. Your system is running smoothly.
|
||||||
|
|
@ -71,10 +68,10 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
||||||
alerts={alerts}
|
alerts={alerts}
|
||||||
machines={machines}
|
machines={machines}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</div>
|
||||||
</Grid>
|
</div>
|
||||||
{!showAllItems && alertsLength > NUM_TO_RENDER && (
|
{!showAllItems && alertsLength > NUM_TO_RENDER && (
|
||||||
<Grid item xs={12}>
|
<div>
|
||||||
<Label1 className="text-center mb-0">
|
<Label1 className="text-center mb-0">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onExpand('alerts')}
|
onClick={() => onExpand('alerts')}
|
||||||
|
|
@ -85,7 +82,7 @@ const Alerts = ({ onReset, onExpand, size }) => {
|
||||||
{`Show all (${alerts.length})`}
|
{`Show all (${alerts.length})`}
|
||||||
</Button>
|
</Button>
|
||||||
</Label1>
|
</Label1>
|
||||||
</Grid>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ const Dashboard = () => {
|
||||||
</TitleSection>
|
</TitleSection>
|
||||||
<div className="flex mb-30 gap-4">
|
<div className="flex mb-30 gap-4">
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1">
|
||||||
<Paper className="p-6">
|
<Paper className="p-6 flex-1">
|
||||||
<SystemPerformance />
|
<SystemPerformance />
|
||||||
</Paper>
|
</Paper>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ const Footer = () => {
|
||||||
const localeFiatCurrency = R.path(['locale_fiatCurrency'])(config) ?? ''
|
const localeFiatCurrency = R.path(['locale_fiatCurrency'])(config) ?? ''
|
||||||
|
|
||||||
const renderFooterItem = key => {
|
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 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 : ''
|
const tickerName = tickerIdx > -1 ? accountsConfig[tickerIdx].display : ''
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useQuery, gql } from '@apollo/client'
|
import { useQuery, gql } from '@apollo/client'
|
||||||
import BigNumber from 'bignumber.js'
|
import BigNumber from 'bignumber.js'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { isAfter } from 'date-fns/fp'
|
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Info2, Label1, Label2, P } from '../../../components/typography/index'
|
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 { fromNamespace } from '../../../utils/config'
|
||||||
import { DAY, WEEK, MONTH } from '../../../utils/time'
|
import { DAY, WEEK, MONTH } from '../../../utils/time'
|
||||||
import { timezones } from '../../../utils/timezone-list'
|
import { timezones } from '../../../utils/timezone-list'
|
||||||
import { toTimezone } from '../../../utils/timezones'
|
|
||||||
|
|
||||||
import PercentageChart from './Graphs/PercentageChart'
|
import PercentageChart from './Graphs/PercentageChart'
|
||||||
import LineChart from './Graphs/RefLineChart'
|
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 getFiats = R.map(R.prop('fiat'))
|
||||||
|
|
||||||
const GET_DATA = gql`
|
const GET_DATA = gql`
|
||||||
query getData($excludeTestingCustomers: Boolean) {
|
query getData($excludeTestingCustomers: Boolean, $from: DateTimeISO) {
|
||||||
transactions(excludeTestingCustomers: $excludeTestingCustomers) {
|
transactions(
|
||||||
|
excludeTestingCustomers: $excludeTestingCustomers
|
||||||
|
from: $from
|
||||||
|
) {
|
||||||
fiatCode
|
fiatCode
|
||||||
fiat
|
fiat
|
||||||
fixedFee
|
fixedFee
|
||||||
|
|
@ -49,10 +50,17 @@ const GET_DATA = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const twoMonthsAgo = new Date()
|
||||||
|
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2)
|
||||||
|
|
||||||
const SystemPerformance = () => {
|
const SystemPerformance = () => {
|
||||||
const [selectedRange, setSelectedRange] = useState('Day')
|
const [selectedRange, setSelectedRange] = useState('Day')
|
||||||
|
|
||||||
const { data, loading } = useQuery(GET_DATA, {
|
const { data, loading } = useQuery(GET_DATA, {
|
||||||
variables: { excludeTestingCustomers: true },
|
variables: {
|
||||||
|
excludeTestingCustomers: true,
|
||||||
|
from: twoMonthsAgo.toISOString(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
const fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
|
const fiatLocale = fromNamespace('locale')(data?.config).fiatCurrency
|
||||||
const timezone = fromNamespace('locale')(data?.config).timezone
|
const timezone = fromNamespace('locale')(data?.config).timezone
|
||||||
|
|
@ -69,38 +77,41 @@ const SystemPerformance = () => {
|
||||||
if (t.error !== null) return false
|
if (t.error !== null) return false
|
||||||
if (t.txClass === 'cashOut' && !t.dispense) return false
|
if (t.txClass === 'cashOut' && !t.dispense) return false
|
||||||
if (t.txClass === 'cashIn' && !t.sendConfirmed) 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 (
|
return (
|
||||||
t.error === null &&
|
t.error === null &&
|
||||||
isAfter(
|
createdTimestamp >= rangeStart - duration &&
|
||||||
toTimezone(t.created, timezone),
|
createdTimestamp < rangeStart
|
||||||
toTimezone(periodDomains[selectedRange][1], timezone),
|
|
||||||
) &&
|
|
||||||
isAfter(
|
|
||||||
toTimezone(periodDomains[selectedRange][0], timezone),
|
|
||||||
toTimezone(t.created, timezone),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
t.error === null &&
|
t.error === null &&
|
||||||
isAfter(
|
createdTimestamp >= rangeStart &&
|
||||||
toTimezone(periodDomains[selectedRange][1], timezone),
|
createdTimestamp <= rangeEnd
|
||||||
toTimezone(t.created, timezone),
|
|
||||||
) &&
|
|
||||||
isAfter(
|
|
||||||
toTimezone(t.created, timezone),
|
|
||||||
toTimezone(periodDomains[selectedRange][0], timezone),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertFiatToLocale = item => {
|
const convertFiatToLocale = item => {
|
||||||
if (item.fiatCode === fiatLocale) return item
|
if (item.fiatCode === fiatLocale)
|
||||||
const itemRate = R.find(R.propEq('code', item.fiatCode))(data.fiatRates)
|
return {
|
||||||
const localeRate = R.find(R.propEq('code', fiatLocale))(data.fiatRates)
|
...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
|
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)(
|
const transactionsToShow = R.map(convertFiatToLocale)(
|
||||||
|
|
@ -140,7 +151,7 @@ const SystemPerformance = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDirectionPercent = () => {
|
const getDirectionPercent = () => {
|
||||||
const [cashIn, cashOut] = R.partition(R.propEq('txClass', 'cashIn'))(
|
const [cashIn, cashOut] = R.partition(R.propEq('cashIn', 'txClass'))(
|
||||||
transactionsToShow,
|
transactionsToShow,
|
||||||
)
|
)
|
||||||
const totalLength = cashIn.length + cashOut.length
|
const totalLength = cashIn.length + cashOut.length
|
||||||
|
|
@ -177,7 +188,10 @@ const SystemPerformance = () => {
|
||||||
handleSetRange={setSelectedRange}
|
handleSetRange={setSelectedRange}
|
||||||
/>
|
/>
|
||||||
{!loading && R.isEmpty(data.transactions) && (
|
{!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) && (
|
{!loading && !R.isEmpty(data.transactions) && (
|
||||||
<div className="flex flex-col gap-12">
|
<div className="flex flex-col gap-12">
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
||||||
initialValues={locale}
|
initialValues={locale}
|
||||||
save={handleSave}
|
save={handleSave}
|
||||||
validationSchema={LocaleSchema}
|
validationSchema={LocaleSchema}
|
||||||
data={R.of(locale)}
|
data={R.of(Array, locale)}
|
||||||
elements={mainFields(data, onChangeCoin)}
|
elements={mainFields(data, onChangeCoin)}
|
||||||
setEditing={onEditingDefault}
|
setEditing={onEditingDefault}
|
||||||
forceDisable={isEditingOverrides}
|
forceDisable={isEditingOverrides}
|
||||||
|
|
@ -238,7 +238,7 @@ const Locales = ({ name: SCREEN_KEY }) => {
|
||||||
{wizard && (
|
{wizard && (
|
||||||
<Wizard
|
<Wizard
|
||||||
schemas={schemas}
|
schemas={schemas}
|
||||||
coin={R.find(R.propEq('code', wizard))(cryptoCurrencies)}
|
coin={R.find(R.propEq(wizard, 'code'))(cryptoCurrencies)}
|
||||||
onClose={() => setWizard(false)}
|
onClose={() => setWizard(false)}
|
||||||
save={wizardSave}
|
save={wizardSave}
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const allFields = (getData, onChange, auxElements = []) => {
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
it => `${R.prop(code)(it)} ${it?.isBeta ? '(Beta)' : ''}`,
|
it => `${R.prop(code)(it)} ${it?.isBeta ? '(Beta)' : ''}`,
|
||||||
R.find(R.propEq(compare ?? 'code', it)),
|
R.find(R.propEq(it, compare ?? 'code')),
|
||||||
)(data)
|
)(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ const allFields = (getData, onChange, auxElements = []) => {
|
||||||
const timezonesData = timezoneList
|
const timezonesData = timezoneList
|
||||||
|
|
||||||
const findSuggestion = it => {
|
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] : []
|
return machine ? [machine] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ const Commissions = ({ name: SCREEN_KEY, id: deviceId }) => {
|
||||||
|
|
||||||
const overrides = config.overrides
|
const overrides = config.overrides
|
||||||
? R.concat(
|
? R.concat(
|
||||||
R.filter(R.propEq('machine', 'ALL_MACHINES'), config.overrides),
|
R.filter(R.propEq('ALL_MACHINES', 'machine'), config.overrides),
|
||||||
R.filter(R.propEq('machine', deviceId), 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
|
const machineID = R.path(['deviceId'])(machine) ?? null
|
||||||
|
|
||||||
return (
|
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">
|
<div className="basis-1/4 min-w-1/4 pt-8">
|
||||||
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
|
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />}>
|
||||||
<Link to="/dashboard" className="no-underline">
|
<Link to="/dashboard" className="no-underline">
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,7 @@ const CashCassettes = () => {
|
||||||
/>
|
/>
|
||||||
{wizard && (
|
{wizard && (
|
||||||
<Wizard
|
<Wizard
|
||||||
machine={R.find(R.propEq('id', machineId), machines)}
|
machine={R.find(R.propEq(machineId, 'id'), machines)}
|
||||||
cashoutSettings={getCashoutSettings(machineId)}
|
cashoutSettings={getCashoutSettings(machineId)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setWizard(false)
|
setWizard(false)
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
view: R.pipe(
|
view: R.pipe(
|
||||||
R.prop('deviceId'),
|
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.defaultTo({ name: <i>Unpaired device</i> }),
|
||||||
R.prop('name'),
|
R.prop('name'),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ const MachineStatus = () => {
|
||||||
]
|
]
|
||||||
|
|
||||||
const machines = R.path(['machines'])(machinesResponse) ?? []
|
const machines = R.path(['machines'])(machinesResponse) ?? []
|
||||||
const expandedIndex = R.findIndex(R.propEq('deviceId', addedMachineId))(
|
const expandedIndex = R.findIndex(R.propEq(addedMachineId, 'deviceId'))(
|
||||||
machines,
|
machines,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
R.pipe(R.pickAll(fields), R.map(defaultToZero))(cassetteInput)
|
R.pipe(R.pickAll(fields), R.map(defaultToZero))(cassetteInput)
|
||||||
|
|
||||||
const onContinue = it => {
|
const onContinue = it => {
|
||||||
const newConfig = R.merge(config, it)
|
const newConfig = R.mergeRight(config, it)
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
const wasCashboxEmptied = [
|
const wasCashboxEmptied = [
|
||||||
config?.wasCashboxEmptied,
|
config?.wasCashboxEmptied,
|
||||||
|
|
@ -158,7 +158,7 @@ const Wizard = ({ machine, cashoutSettings, locale, onClose, save, error }) => {
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const makeInitialValues = () =>
|
const makeInitialValues = () =>
|
||||||
R.merge(makeCassettesInitialValues(), makeRecyclersInitialValues())
|
R.mergeRight(makeCassettesInitialValues(), makeRecyclersInitialValues())
|
||||||
|
|
||||||
const steps = R.pipe(
|
const steps = R.pipe(
|
||||||
R.concat(
|
R.concat(
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,12 @@ const CryptoBalanceOverrides = ({ section }) => {
|
||||||
|
|
||||||
const overriddenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(setupValues)
|
const overriddenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(setupValues)
|
||||||
const suggestionFilter = R.filter(
|
const suggestionFilter = R.filter(
|
||||||
it => !R.contains(it.code, overriddenCryptos),
|
it => !R.includes(it.code, overriddenCryptos),
|
||||||
)
|
)
|
||||||
const suggestions = suggestionFilter(cryptoCurrencies)
|
const suggestions = suggestionFilter(cryptoCurrencies)
|
||||||
|
|
||||||
const findSuggestion = it => {
|
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,
|
cryptoCurrencies,
|
||||||
)
|
)
|
||||||
return coin ? [coin] : []
|
return coin ? [coin] : []
|
||||||
|
|
@ -90,7 +90,7 @@ const CryptoBalanceOverrides = ({ section }) => {
|
||||||
const viewCrypto = it =>
|
const viewCrypto = it =>
|
||||||
R.compose(
|
R.compose(
|
||||||
R.path(['display']),
|
R.path(['display']),
|
||||||
R.find(R.propEq('code', it)),
|
R.find(R.propEq(it, 'code')),
|
||||||
)(cryptoCurrencies)
|
)(cryptoCurrencies)
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const findSuggestion = it => {
|
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] : []
|
return coin ? [coin] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,7 +127,7 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const viewMachine = it =>
|
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(
|
const elements = R.concat(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ const ThirdPartyProvider = () => {
|
||||||
const getDisplayName = type => it =>
|
const getDisplayName = type => it =>
|
||||||
R.compose(
|
R.compose(
|
||||||
R.prop('display'),
|
R.prop('display'),
|
||||||
R.find(R.propEq('code', it)),
|
R.find(R.propEq(it, 'code')),
|
||||||
)(filterOptions(type))
|
)(filterOptions(type))
|
||||||
|
|
||||||
const innerSave = async value => {
|
const innerSave = async value => {
|
||||||
|
|
@ -73,7 +73,7 @@ const ThirdPartyProvider = () => {
|
||||||
<EditableTable
|
<EditableTable
|
||||||
name="thirdParty"
|
name="thirdParty"
|
||||||
initialValues={values}
|
initialValues={values}
|
||||||
data={R.of(values)}
|
data={R.of(Array, values)}
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
enableEdit
|
enableEdit
|
||||||
editWidth={174}
|
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 findValue = name => findField(name).value
|
||||||
|
|
||||||
const displayTextValue = value => 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 findValue = name => findField(name).value
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const FormRenderer = ({
|
||||||
R.map(({ code }) => ({ [code]: (value && value[code]) ?? '' })),
|
R.map(({ code }) => ({ [code]: (value && value[code]) ?? '' })),
|
||||||
)(elements)
|
)(elements)
|
||||||
|
|
||||||
const values = R.merge(initialValues, value)
|
const values = R.mergeRight(initialValues, value)
|
||||||
|
|
||||||
const [saveError, setSaveError] = useState([])
|
const [saveError, setSaveError] = useState([])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ const Services = () => {
|
||||||
|
|
||||||
const getAccounts = ({ elements, code }) => {
|
const getAccounts = ({ elements, code }) => {
|
||||||
const account = accounts[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 mapToCode = R.map(R.prop(['code']))
|
||||||
const passwordFields = R.compose(
|
const passwordFields = R.compose(
|
||||||
mapToCode,
|
mapToCode,
|
||||||
|
|
|
||||||
|
|
@ -70,13 +70,13 @@ const Triggers = () => {
|
||||||
const enabledCustomInfoRequests = R.pipe(
|
const enabledCustomInfoRequests = R.pipe(
|
||||||
R.path(['customInfoRequests']),
|
R.path(['customInfoRequests']),
|
||||||
R.defaultTo([]),
|
R.defaultTo([]),
|
||||||
R.filter(R.propEq('enabled', true)),
|
R.filter(R.propEq(true, 'enabled')),
|
||||||
)(customInfoReqData)
|
)(customInfoReqData)
|
||||||
|
|
||||||
const emailAuth =
|
const emailAuth =
|
||||||
data?.config?.triggersConfig_customerAuthentication === 'EMAIL'
|
data?.config?.triggersConfig_customerAuthentication === 'EMAIL'
|
||||||
|
|
||||||
const complianceServices = R.filter(R.propEq('class', 'compliance'))(
|
const complianceServices = R.filter(R.propEq('compliance', 'class'))(
|
||||||
data?.accountsConfig || [],
|
data?.accountsConfig || [],
|
||||||
)
|
)
|
||||||
const triggers = fromServer(data?.config?.triggers ?? [])
|
const triggers = fromServer(data?.config?.triggers ?? [])
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ const Wizard = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
const onContinue = async it => {
|
const onContinue = async it => {
|
||||||
const newConfig = R.merge(config, stepOptions.schema.cast(it))
|
const newConfig = R.mergeRight(config, stepOptions.schema.cast(it))
|
||||||
|
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
return save(newConfig)
|
return save(newConfig)
|
||||||
|
|
@ -221,7 +221,7 @@ const Wizard = ({
|
||||||
|
|
||||||
const createErrorMessage = (errors, touched, values) => {
|
const createErrorMessage = (errors, touched, values) => {
|
||||||
const triggerType = values?.triggerType
|
const triggerType = values?.triggerType
|
||||||
const containsType = R.contains(triggerType)
|
const containsType = R.includes(triggerType)
|
||||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||||
const isCustom = values?.requirement?.requirement === 'custom'
|
const isCustom = values?.requirement?.requirement === 'custom'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ const AdvancedTriggersSettings = memo(() => {
|
||||||
|
|
||||||
const customInfoRequests =
|
const customInfoRequests =
|
||||||
R.path(['customInfoRequests'])(customInfoReqData) ?? []
|
R.path(['customInfoRequests'])(customInfoReqData) ?? []
|
||||||
const enabledCustomInfoRequests = R.filter(R.propEq('enabled', true))(
|
const enabledCustomInfoRequests = R.filter(R.propEq(true, 'enabled'))(
|
||||||
customInfoRequests,
|
customInfoRequests,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ const AdvancedTriggersSettings = memo(() => {
|
||||||
initialValues={requirementsDefaults}
|
initialValues={requirementsDefaults}
|
||||||
save={saveDefaults}
|
save={saveDefaults}
|
||||||
validationSchema={defaultSchema}
|
validationSchema={defaultSchema}
|
||||||
data={R.of(requirementsDefaults)}
|
data={R.of(Array, requirementsDefaults)}
|
||||||
elements={getDefaultSettings()}
|
elements={getDefaultSettings()}
|
||||||
setEditing={onEditingDefault}
|
setEditing={onEditingDefault}
|
||||||
forceDisable={isEditingOverrides}
|
forceDisable={isEditingOverrides}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const buildAdvancedRequirementOptions = customInfoRequests => {
|
||||||
const displayRequirement = (code, customInfoRequests) => {
|
const displayRequirement = (code, customInfoRequests) => {
|
||||||
return R.prop(
|
return R.prop(
|
||||||
'display',
|
'display',
|
||||||
R.find(R.propEq('code', code))(
|
R.find(R.propEq(code, 'code'))(
|
||||||
buildAdvancedRequirementOptions(customInfoRequests),
|
buildAdvancedRequirementOptions(customInfoRequests),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -47,7 +47,7 @@ const getOverridesSchema = (values, customInfoRequests) => {
|
||||||
const { id, requirement } = this.parent
|
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
|
// 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)
|
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({
|
return this.createError({
|
||||||
message: `Requirement '${displayRequirement(
|
message: `Requirement '${displayRequirement(
|
||||||
requirement,
|
requirement,
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ const Type = ({ ...props }) => {
|
||||||
'text-tomato': errors.triggerType && touched.triggerType,
|
'text-tomato': errors.triggerType && touched.triggerType,
|
||||||
}
|
}
|
||||||
|
|
||||||
const containsType = R.contains(values?.triggerType)
|
const containsType = R.includes(values?.triggerType)
|
||||||
const isThresholdCurrencyEnabled = containsType(['txAmount', 'txVolume'])
|
const isThresholdCurrencyEnabled = containsType(['txAmount', 'txVolume'])
|
||||||
const isTransactionAmountEnabled = containsType(['txVelocity'])
|
const isTransactionAmountEnabled = containsType(['txVelocity'])
|
||||||
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
||||||
|
|
@ -542,7 +542,7 @@ const requirements = (
|
||||||
const getView = (data, code, compare) => it => {
|
const getView = (data, code, compare) => it => {
|
||||||
if (!data) return ''
|
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 => {
|
const customReqIdMatches = customReqId => it => {
|
||||||
|
|
|
||||||
|
|
@ -69,12 +69,12 @@ const AdvancedWallet = () => {
|
||||||
AdvancedWalletSettingsOverrides,
|
AdvancedWalletSettingsOverrides,
|
||||||
)
|
)
|
||||||
const suggestionFilter = R.filter(
|
const suggestionFilter = R.filter(
|
||||||
it => !R.contains(it.code, overriddenCryptos),
|
it => !R.includes(it.code, overriddenCryptos),
|
||||||
)
|
)
|
||||||
const coinSuggestions = suggestionFilter(cryptoCurrencies)
|
const coinSuggestions = suggestionFilter(cryptoCurrencies)
|
||||||
|
|
||||||
const findSuggestion = it => {
|
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,
|
cryptoCurrencies,
|
||||||
)
|
)
|
||||||
return coin ? [coin] : []
|
return coin ? [coin] : []
|
||||||
|
|
@ -85,13 +85,13 @@ const AdvancedWallet = () => {
|
||||||
<Section>
|
<Section>
|
||||||
<EditableTable
|
<EditableTable
|
||||||
name="wallets"
|
name="wallets"
|
||||||
data={R.of(AdvancedWalletSettings)}
|
data={R.of(Array, AdvancedWalletSettings)}
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
enableEdit
|
enableEdit
|
||||||
editWidth={174}
|
editWidth={174}
|
||||||
save={save}
|
save={save}
|
||||||
stripeWhen={it => !AdvancedWalletSchema.isValidSync(it)}
|
stripeWhen={it => !AdvancedWalletSchema.isValidSync(it)}
|
||||||
inialValues={R.of(AdvancedWalletSettings)}
|
inialValues={R.of(Array, AdvancedWalletSettings)}
|
||||||
validationSchema={AdvancedWalletSchema}
|
validationSchema={AdvancedWalletSchema}
|
||||||
elements={getAdvancedWalletElements()}
|
elements={getAdvancedWalletElements()}
|
||||||
setEditing={onEditingDefault}
|
setEditing={onEditingDefault}
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
||||||
/>
|
/>
|
||||||
{wizard && (
|
{wizard && (
|
||||||
<Wizard
|
<Wizard
|
||||||
coin={R.find(R.propEq('code', wizard))(cryptoCurrencies)}
|
coin={R.find(R.propEq(wizard, 'code'))(cryptoCurrencies)}
|
||||||
onClose={() => setWizard(false)}
|
onClose={() => setWizard(false)}
|
||||||
save={save}
|
save={save}
|
||||||
schemas={schemas}
|
schemas={schemas}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import { has0Conf } from './helper'
|
||||||
const MAX_STEPS = 5
|
const MAX_STEPS = 5
|
||||||
const MODAL_WIDTH = 554
|
const MODAL_WIDTH = 554
|
||||||
|
|
||||||
const contains = crypto => R.compose(R.contains(crypto), R.prop('cryptos'))
|
const contains = crypto => R.compose(R.includes(crypto), R.prop('cryptos'))
|
||||||
const sameClass = type => R.propEq('class', type)
|
const sameClass = type => R.propEq(type, 'class')
|
||||||
const filterConfig = (crypto, type) =>
|
const filterConfig = (crypto, type) =>
|
||||||
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
|
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
|
||||||
const removeDeprecated = R.filter(({ deprecated }) => !deprecated)
|
const removeDeprecated = R.filter(({ deprecated }) => !deprecated)
|
||||||
|
|
@ -59,7 +59,7 @@ const Wizard = ({
|
||||||
const exchanges = getItems(accountsConfig, accounts, 'exchange', coin.code)
|
const exchanges = getItems(accountsConfig, accounts, 'exchange', coin.code)
|
||||||
const zeroConfs = getItems(accountsConfig, accounts, 'zeroConf', 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 = [
|
const commonWizardSteps = [
|
||||||
{ type: 'ticker', ...tickers },
|
{ type: 'ticker', ...tickers },
|
||||||
|
|
@ -99,9 +99,9 @@ const Wizard = ({
|
||||||
const stepData = step > 0 ? wizardSteps[step - 1] : null
|
const stepData = step > 0 ? wizardSteps[step - 1] : null
|
||||||
|
|
||||||
const onContinue = async (stepConfig, stepAccount) => {
|
const onContinue = async (stepConfig, stepAccount) => {
|
||||||
const newConfig = R.merge(config, stepConfig)
|
const newConfig = R.mergeRight(config, stepConfig)
|
||||||
const newAccounts = stepAccount
|
const newAccounts = stepAccount
|
||||||
? R.merge(accountsToSave, stepAccount)
|
? R.mergeRight(accountsToSave, stepAccount)
|
||||||
: accountsToSave
|
: accountsToSave
|
||||||
|
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const reducer = (state, action) => {
|
||||||
iError: false,
|
iError: false,
|
||||||
}
|
}
|
||||||
case 'error':
|
case 'error':
|
||||||
return R.merge(state, { innerError: true })
|
return R.mergeRight(state, { innerError: true })
|
||||||
case 'reset':
|
case 'reset':
|
||||||
return initialState
|
return initialState
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { CURRENCY_MAX } from '../../utils/constants'
|
||||||
import { defaultToZero } from '../../utils/number'
|
import { defaultToZero } from '../../utils/number'
|
||||||
|
|
||||||
const filterClass = type => R.filter(it => it.class === type)
|
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({
|
const WalletSchema = Yup.object().shape({
|
||||||
ticker: Yup.string('The ticker must be a string').required(
|
ticker: Yup.string('The ticker must be a string').required(
|
||||||
|
|
@ -36,6 +36,7 @@ const AdvancedWalletSchema = Yup.object().shape({
|
||||||
cryptoUnits: Yup.string().required(),
|
cryptoUnits: Yup.string().required(),
|
||||||
feeMultiplier: Yup.string().required(),
|
feeMultiplier: Yup.string().required(),
|
||||||
allowTransactionBatching: Yup.boolean(),
|
allowTransactionBatching: Yup.boolean(),
|
||||||
|
enableLastUsedAddress: Yup.boolean(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const OverridesSchema = Yup.object().shape({
|
const OverridesSchema = Yup.object().shape({
|
||||||
|
|
@ -57,7 +58,7 @@ const OverridesDefaults = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewFeeMultiplier = it =>
|
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 = [
|
const feeOptions = [
|
||||||
{ display: '+60%', code: '1.6' },
|
{ display: '+60%', code: '1.6' },
|
||||||
|
|
@ -127,6 +128,17 @@ const getAdvancedWalletElements = () => {
|
||||||
labelProp: 'display',
|
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 viewCryptoCurrency = it => {
|
||||||
const currencyDisplay = R.compose(
|
const currencyDisplay = R.compose(
|
||||||
it => `${R.prop(['display'])(it)} ${it?.isBeta ? '(Beta)' : ''}`,
|
it => `${R.prop(['display'])(it)} ${it?.isBeta ? '(Beta)' : ''}`,
|
||||||
R.find(R.propEq('code', it)),
|
R.find(R.propEq(it, 'code')),
|
||||||
)(cryptoCurrencies)
|
)(cryptoCurrencies)
|
||||||
return currencyDisplay
|
return currencyDisplay
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +225,7 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
||||||
const getDisplayName = type => it =>
|
const getDisplayName = type => it =>
|
||||||
R.compose(
|
R.compose(
|
||||||
R.prop('display'),
|
R.prop('display'),
|
||||||
R.find(R.propEq('code', it)),
|
R.find(R.propEq(it, 'code')),
|
||||||
)(filterOptions(type))
|
)(filterOptions(type))
|
||||||
|
|
||||||
const getOptions = R.curry((option, it) =>
|
const getOptions = R.curry((option, it) =>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const SAVE_ACCOUNTS = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const isConfigurable = it =>
|
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 ChooseExchange = ({ data: currentData, addData }) => {
|
||||||
const { data } = useQuery(GET_CONFIG)
|
const { data } = useQuery(GET_CONFIG)
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,10 @@ const SAVE_ACCOUNTS = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const isConfigurable = it =>
|
const isConfigurable = it =>
|
||||||
R.contains(it)(['infura', 'bitgo', 'trongrid', 'galoy'])
|
R.includes(it)(['infura', 'bitgo', 'trongrid', 'galoy'])
|
||||||
|
|
||||||
const isLocalHosted = it =>
|
const isLocalHosted = it =>
|
||||||
R.contains(it)([
|
R.includes(it)([
|
||||||
'bitcoind',
|
'bitcoind',
|
||||||
'geth',
|
'geth',
|
||||||
'litecoind',
|
'litecoind',
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const Wallet = ({ doContinue }) => {
|
||||||
const Component = mySteps[step].component
|
const Component = mySteps[step].component
|
||||||
|
|
||||||
const addData = it => {
|
const addData = it => {
|
||||||
setData(R.merge(data, it))
|
setData(R.mergeRight(data, it))
|
||||||
setStep(step + 1)
|
setStep(step + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import * as R from 'ramda'
|
||||||
|
|
||||||
import _schema from '../../../Services/schemas'
|
import _schema from '../../../Services/schemas'
|
||||||
|
|
||||||
const contains = crypto => R.compose(R.contains(crypto), R.prop('cryptos'))
|
const contains = crypto => R.compose(R.includes(crypto), R.prop('cryptos'))
|
||||||
const sameClass = type => R.propEq('class', type)
|
const sameClass = type => R.propEq(type, 'class')
|
||||||
const filterConfig = (crypto, type) =>
|
const filterConfig = (crypto, type) =>
|
||||||
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
|
R.filter(it => sameClass(type)(it) && contains(crypto)(it))
|
||||||
export const getItems = (accountsConfig, accounts, type, crypto) => {
|
export const getItems = (accountsConfig, accounts, type, crypto) => {
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,11 @@ const hasSidebar = route =>
|
||||||
const getParent = route =>
|
const getParent = route =>
|
||||||
R.find(
|
R.find(
|
||||||
R.propEq(
|
R.propEq(
|
||||||
'route',
|
|
||||||
R.dropLast(
|
R.dropLast(
|
||||||
1,
|
1,
|
||||||
R.dropLastWhile(x => x !== '/', route),
|
R.dropLastWhile(x => x !== '/', route),
|
||||||
),
|
),
|
||||||
|
'route',
|
||||||
),
|
),
|
||||||
)(flattened)
|
)(flattened)
|
||||||
|
|
||||||
|
|
@ -69,8 +69,8 @@ const Routes = () => {
|
||||||
Transition === Slide
|
Transition === Slide
|
||||||
? {
|
? {
|
||||||
direction:
|
direction:
|
||||||
R.findIndex(R.propEq('route', history.state.prev))(leafRoutes) >
|
R.findIndex(R.propEq(history.state.prev, 'route'))(leafRoutes) >
|
||||||
R.findIndex(R.propEq('route', location))(leafRoutes)
|
R.findIndex(R.propEq(location, 'route'))(leafRoutes)
|
||||||
? 'right'
|
? 'right'
|
||||||
: 'left',
|
: 'left',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ const { p } = typographyStyles
|
||||||
let theme = createTheme({
|
let theme = createTheme({
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: inputFontFamily,
|
fontFamily: inputFontFamily,
|
||||||
|
root: { ...p },
|
||||||
|
body1: { ...p },
|
||||||
},
|
},
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
|
|
@ -56,6 +58,18 @@ theme = createTheme(theme, {
|
||||||
body1: { ...p },
|
body1: { ...p },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiCircularProgress: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
color: primaryColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTableCell: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: { ...p },
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiIconButtonBase: {
|
MuiIconButtonBase: {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
disableRipple: true,
|
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
|
// no need to log the migration process
|
||||||
process.env.SKIP_SERVER_LOGS = true
|
process.env.SKIP_SERVER_LOGS = true
|
||||||
|
|
||||||
db.none(createMigration)
|
function checkPostgresVersion () {
|
||||||
.then(() => migrate.run())
|
return db.one('SHOW server_version;')
|
||||||
.then(() => {
|
.then(result => {
|
||||||
console.log('DB Migration succeeded.')
|
console.log(result)
|
||||||
process.exit(0)
|
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 => {
|
.catch(err => {
|
||||||
console.error('DB Migration failed: %s', err)
|
console.error('DB Migration failed: %s', err)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const logger = require('../logger')
|
||||||
const settingsLoader = require('../new-settings-loader')
|
const settingsLoader = require('../new-settings-loader')
|
||||||
const configManager = require('../new-config-manager')
|
const configManager = require('../new-config-manager')
|
||||||
const notifier = require('../notifier')
|
const notifier = require('../notifier')
|
||||||
|
const constants = require('../constants')
|
||||||
|
|
||||||
const cashInAtomic = require('./cash-in-atomic')
|
const cashInAtomic = require('./cash-in-atomic')
|
||||||
const cashInLow = require('./cash-in-low')
|
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) {
|
function doesTxReuseAddress(tx) {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT EXISTS (
|
SELECT COUNT(*) > 1 as exists
|
||||||
SELECT DISTINCT to_address FROM (
|
FROM (SELECT DISTINCT customer_id
|
||||||
SELECT to_address FROM cash_in_txs WHERE id != $1
|
FROM cash_in_txs
|
||||||
) AS x WHERE to_address = $2
|
WHERE to_address = $1
|
||||||
)`
|
AND customer_id != $3
|
||||||
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
|
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) {
|
function getWalletScore(tx, pi) {
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,12 @@
|
||||||
*/
|
*/
|
||||||
const prepare_denominations = denominations =>
|
const prepare_denominations = denominations =>
|
||||||
JSON.parse(JSON.stringify(denominations))
|
JSON.parse(JSON.stringify(denominations))
|
||||||
.sort(([d1], [d2]) => d1 < d2)
|
.sort(([d1], [d2]) => d2 - d1)
|
||||||
.reduce(
|
.reduce(
|
||||||
([csum, denoms], [denom, count]) => {
|
([csum, denoms], [denom, count]) => {
|
||||||
csum += denom * count
|
csum += denom * count
|
||||||
return [csum, [{ denom, count, csum }].concat(denoms)]
|
denoms.push({ denom, count, csum })
|
||||||
|
return [csum, denoms]
|
||||||
},
|
},
|
||||||
[0, []],
|
[0, []],
|
||||||
)[1] /* ([csum, denoms]) => denoms */
|
)[1] /* ([csum, denoms]) => denoms */
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,17 @@ const fs = require('fs')
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const anonymous = require('../lib/constants').anonymousCustomer
|
|
||||||
const complianceOverrides = require('./compliance_overrides')
|
const complianceOverrides = require('./compliance_overrides')
|
||||||
const writeFile = util.promisify(fs.writeFile)
|
const writeFile = util.promisify(fs.writeFile)
|
||||||
const notifierQueries = require('./notifier/queries')
|
const notifierQueries = require('./notifier/queries')
|
||||||
const notifierUtils = require('./notifier/utils')
|
const notifierUtils = require('./notifier/utils')
|
||||||
const NUM_RESULTS = 1000
|
|
||||||
const sms = require('./sms')
|
const sms = require('./sms')
|
||||||
const settingsLoader = require('./new-settings-loader')
|
const settingsLoader = require('./new-settings-loader')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const externalCompliance = require('./compliance-external')
|
const externalCompliance = require('./compliance-external')
|
||||||
|
const {
|
||||||
|
customers: { getCustomerList },
|
||||||
|
} = require('typesafe-db')
|
||||||
|
|
||||||
const { APPROVED, RETRY } = require('./plugins/compliance/consts')
|
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) {
|
function getSlimCustomerByIdBatch(ids) {
|
||||||
const sql = `SELECT id, phone, id_card_data
|
const sql = `SELECT id, phone, id_card_data
|
||||||
FROM customers
|
FROM customers
|
||||||
|
|
@ -512,88 +491,8 @@ function getSlimCustomerByIdBatch(ids) {
|
||||||
return db.any(sql, [ids]).then(customers => _.map(camelize, customers))
|
return db.any(sql, [ids]).then(customers => _.map(camelize, customers))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: getCustomersList and getCustomerById are very similar, so this should be refactored
|
function getCustomersList() {
|
||||||
|
return getCustomerList({ withCustomInfoRequest: true })
|
||||||
/**
|
|
||||||
* 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1081,12 +980,10 @@ function notifyApprovedExternalCompliance(settings, customerId) {
|
||||||
|
|
||||||
function checkExternalCompliance(settings) {
|
function checkExternalCompliance(settings) {
|
||||||
return getOpenExternalCompliance().then(externals => {
|
return getOpenExternalCompliance().then(externals => {
|
||||||
console.log(externals)
|
|
||||||
const promises = _.map(external => {
|
const promises = _.map(external => {
|
||||||
return externalCompliance
|
return externalCompliance
|
||||||
.getStatus(settings, external.service, external.customer_id)
|
.getStatus(settings, external.service, external.customer_id)
|
||||||
.then(status => {
|
.then(status => {
|
||||||
console.log('status', status, external.customer_id, external.service)
|
|
||||||
if (status.status.answer === RETRY)
|
if (status.status.answer === RETRY)
|
||||||
notifyRetryExternalCompliance(
|
notifyRetryExternalCompliance(
|
||||||
settings,
|
settings,
|
||||||
|
|
@ -1112,12 +1009,16 @@ function addExternalCompliance(customerId, service, id) {
|
||||||
return db.none(sql, [customerId, id, service])
|
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 = {
|
module.exports = {
|
||||||
add,
|
add,
|
||||||
addWithEmail,
|
addWithEmail,
|
||||||
get,
|
get,
|
||||||
getWithEmail,
|
getWithEmail,
|
||||||
batch,
|
|
||||||
getSlimCustomerByIdBatch,
|
getSlimCustomerByIdBatch,
|
||||||
getCustomersList,
|
getCustomersList,
|
||||||
getCustomerById,
|
getCustomerById,
|
||||||
|
|
@ -1139,4 +1040,5 @@ module.exports = {
|
||||||
updateLastAuthAttempt,
|
updateLastAuthAttempt,
|
||||||
addExternalCompliance,
|
addExternalCompliance,
|
||||||
checkExternalCompliance,
|
checkExternalCompliance,
|
||||||
|
getLastUsedAddress,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ const eventBus = require('./event-bus')
|
||||||
const DATABASE_NOT_REACHABLE = 'Database not reachable.'
|
const DATABASE_NOT_REACHABLE = 'Database not reachable.'
|
||||||
|
|
||||||
const pgp = Pgp({
|
const pgp = Pgp({
|
||||||
pgNative: true,
|
|
||||||
schema: 'public',
|
schema: 'public',
|
||||||
error: (err, e) => {
|
error: (err, e) => {
|
||||||
if (e.cn) logger.error(DATABASE_NOT_REACHABLE)
|
if (e.cn) logger.error(DATABASE_NOT_REACHABLE)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ function getBitPayFxRate(
|
||||||
fiatCodeProperty,
|
fiatCodeProperty,
|
||||||
rateProperty,
|
rateProperty,
|
||||||
) {
|
) {
|
||||||
return getFiatRates().then(({ data: fxRates }) => {
|
return getFiatRates().then(fxRates => {
|
||||||
const defaultFiatRate = findCurrencyRates(
|
const defaultFiatRate = findCurrencyRates(
|
||||||
fxRates,
|
fxRates,
|
||||||
defaultFiatMarket,
|
defaultFiatMarket,
|
||||||
|
|
@ -69,14 +69,15 @@ const getRate = (retries = 1, fiatCode, defaultFiatMarket) => {
|
||||||
defaultFiatMarket,
|
defaultFiatMarket,
|
||||||
fiatCodeProperty,
|
fiatCodeProperty,
|
||||||
rateProperty,
|
rateProperty,
|
||||||
).catch(() => {
|
).catch(err => {
|
||||||
// Switch service
|
|
||||||
const erroredService = API_QUEUE.shift()
|
const erroredService = API_QUEUE.shift()
|
||||||
API_QUEUE.push(erroredService)
|
API_QUEUE.push(erroredService)
|
||||||
if (retries >= MAX_ROTATIONS)
|
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) {
|
function setMachine(rec, operatorId) {
|
||||||
rec.operatorId = operatorId
|
rec.operatorId = operatorId
|
||||||
switch (rec.action) {
|
switch (rec.action) {
|
||||||
|
|
@ -681,4 +718,5 @@ module.exports = {
|
||||||
refillMachineUnits,
|
refillMachineUnits,
|
||||||
updateDiagnostics,
|
updateDiagnostics,
|
||||||
updateFailedQRScans,
|
updateFailedQRScans,
|
||||||
|
batchDiagnostics,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,5 @@ function transaction() {
|
||||||
|
|
||||||
return db.any(sql)
|
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 }
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { transaction, customer }
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
const authentication = require('../modules/userManagement')
|
const authentication = require('../modules/userManagement')
|
||||||
const anonymous = require('../../../constants').anonymousCustomer
|
const anonymous = require('../../../constants').anonymousCustomer
|
||||||
const customers = require('../../../customers')
|
const customers = require('../../../customers')
|
||||||
const filters = require('../../filters')
|
|
||||||
const customerNotes = require('../../../customer-notes')
|
const customerNotes = require('../../../customer-notes')
|
||||||
const machineLoader = require('../../../machine-loader')
|
const machineLoader = require('../../../machine-loader')
|
||||||
|
|
||||||
|
|
@ -18,11 +17,9 @@ const resolvers = {
|
||||||
isAnonymous: parent => parent.customerId === anonymous.uuid,
|
isAnonymous: parent => parent.customerId === anonymous.uuid,
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
customers: (...[, { phone, email, name, address, id }]) =>
|
customers: () => customers.getCustomersList(),
|
||||||
customers.getCustomersList(phone, name, address, id, email),
|
|
||||||
customer: (...[, { customerId }]) =>
|
customer: (...[, { customerId }]) =>
|
||||||
customers.getCustomerById(customerId).then(addLastUsedMachineName),
|
customers.getCustomerById(customerId).then(addLastUsedMachineName),
|
||||||
customerFilters: () => filters.customer(),
|
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
setCustomer: (root, { customerId, customerInput }, context) => {
|
setCustomer: (root, { customerId, customerInput }, context) => {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ function ticker(fiatCode, cryptoCode, tickerName) {
|
||||||
return getCurrencyRates(ticker, fiatCode, cryptoCode)
|
return getCurrencyRates(ticker, fiatCode, cryptoCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return getRate(RETRIES, tickerName, defaultFiatMarket(tickerName)).then(
|
return getRate(RETRIES, fiatCode, defaultFiatMarket(tickerName)).then(
|
||||||
({ fxRate }) => {
|
({ fxRate }) => {
|
||||||
try {
|
try {
|
||||||
return getCurrencyRates(
|
return getCurrencyRates(
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ const loadRoutes = async () => {
|
||||||
app.use(compression({ threshold: 500 }))
|
app.use(compression({ threshold: 500 }))
|
||||||
app.use(helmet())
|
app.use(helmet())
|
||||||
app.use(nocache())
|
app.use(nocache())
|
||||||
app.use(express.json({ limit: '2mb' }))
|
app.use(express.json({ limit: '25mb' }))
|
||||||
|
|
||||||
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
||||||
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,13 @@ function getExternalComplianceLink(req, res, next) {
|
||||||
.then(url => respond(req, res, { url }))
|
.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 triggers = configManager.getTriggers(config)
|
||||||
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
|
const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
|
||||||
|
|
||||||
|
|
@ -346,6 +352,18 @@ function addOrUpdateCustomer(customerData, deviceId, config, isEmailAuth) {
|
||||||
.getCustomerActiveIndividualDiscount(customer.id)
|
.getCustomerActiveIndividualDiscount(customer.id)
|
||||||
.then(discount => ({ ...customer, discount }))
|
.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) {
|
function getOrAddCustomerPhone(req, res, next) {
|
||||||
|
|
@ -354,6 +372,7 @@ function getOrAddCustomerPhone(req, res, next) {
|
||||||
|
|
||||||
const pi = plugins(req.settings, deviceId)
|
const pi = plugins(req.settings, deviceId)
|
||||||
const phone = req.body.phone
|
const phone = req.body.phone
|
||||||
|
const cryptoCode = req.query.cryptoCode
|
||||||
|
|
||||||
return pi
|
return pi
|
||||||
.getPhoneCode(phone)
|
.getPhoneCode(phone)
|
||||||
|
|
@ -363,6 +382,7 @@ function getOrAddCustomerPhone(req, res, next) {
|
||||||
deviceId,
|
deviceId,
|
||||||
req.settings.config,
|
req.settings.config,
|
||||||
false,
|
false,
|
||||||
|
cryptoCode,
|
||||||
).then(customer => respond(req, res, { code, customer }))
|
).then(customer => respond(req, res, { code, customer }))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
@ -375,6 +395,7 @@ function getOrAddCustomerPhone(req, res, next) {
|
||||||
function getOrAddCustomerEmail(req, res, next) {
|
function getOrAddCustomerEmail(req, res, next) {
|
||||||
const deviceId = req.deviceId
|
const deviceId = req.deviceId
|
||||||
const customerData = req.body
|
const customerData = req.body
|
||||||
|
const cryptoCode = req.query.cryptoCode
|
||||||
|
|
||||||
const pi = plugins(req.settings, req.deviceId)
|
const pi = plugins(req.settings, req.deviceId)
|
||||||
const email = req.body.email
|
const email = req.body.email
|
||||||
|
|
@ -387,6 +408,7 @@ function getOrAddCustomerEmail(req, res, next) {
|
||||||
deviceId,
|
deviceId,
|
||||||
req.settings.config,
|
req.settings.config,
|
||||||
true,
|
true,
|
||||||
|
cryptoCode,
|
||||||
).then(customer => respond(req, res, { code, customer }))
|
).then(customer => respond(req, res, { code, customer }))
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,12 @@ function getTx(req, res, next) {
|
||||||
return helpers
|
return helpers
|
||||||
.fetchStatusTx(req.params.id, req.query.status)
|
.fetchStatusTx(req.params.id, req.query.status)
|
||||||
.then(r => res.json(r))
|
.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))
|
return next(httpError('Not Found', 404))
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const T = require('./time')
|
||||||
// FP operations on Postgres result in very big errors.
|
// FP operations on Postgres result in very big errors.
|
||||||
// E.g.: 1853.013808 * 1000 = 1866149.494
|
// E.g.: 1853.013808 * 1000 = 1866149.494
|
||||||
const REDEEMABLE_AGE = T.day / 1000
|
const REDEEMABLE_AGE = T.day / 1000
|
||||||
|
const MAX_THRESHOLD_DAYS = 365 * 50 // 50 years maximum
|
||||||
|
|
||||||
function process(tx, pi) {
|
function process(tx, pi) {
|
||||||
const mtx = massage(tx)
|
const mtx = massage(tx)
|
||||||
|
|
@ -92,7 +93,9 @@ function customerHistory(customerId, thresholdDays) {
|
||||||
AND fiat > 0
|
AND fiat > 0
|
||||||
) ch WHERE NOT ch.expired ORDER BY ch.created`
|
) 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])
|
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-each-series": "^1.0.0",
|
||||||
"p-queue": "^6.6.2",
|
"p-queue": "^6.6.2",
|
||||||
"p-retry": "^4.4.0",
|
"p-retry": "^4.4.0",
|
||||||
"pg-native": "^3.0.0",
|
|
||||||
"pg-promise": "^10.10.2",
|
"pg-promise": "^10.10.2",
|
||||||
"pify": "^3.0.0",
|
"pify": "^3.0.0",
|
||||||
"pretty-ms": "^2.1.0",
|
"pretty-ms": "^2.1.0",
|
||||||
|
|
@ -89,6 +88,7 @@
|
||||||
"telnyx": "^1.25.5",
|
"telnyx": "^1.25.5",
|
||||||
"tronweb": "^5.3.0",
|
"tronweb": "^5.3.0",
|
||||||
"twilio": "^3.6.1",
|
"twilio": "^3.6.1",
|
||||||
|
"typesafe-db": "workspace:*",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"web3": "1.7.1",
|
"web3": "1.7.1",
|
||||||
"winston": "^2.4.2",
|
"winston": "^2.4.2",
|
||||||
|
|
@ -123,34 +123,16 @@
|
||||||
"lamassu-eth-recovery": "./bin/lamassu-eth-recovery",
|
"lamassu-eth-recovery": "./bin/lamassu-eth-recovery",
|
||||||
"lamassu-trx-recovery": "./bin/lamassu-trx-recovery",
|
"lamassu-trx-recovery": "./bin/lamassu-trx-recovery",
|
||||||
"lamassu-update-cassettes": "./bin/lamassu-update-cassettes",
|
"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": {
|
"scripts": {
|
||||||
"start": "node bin/lamassu-server",
|
"dev": "concurrently \"npm:server\" \"npm:admin-server\"",
|
||||||
"test": "mocha --recursive tests",
|
"server": "node --watch bin/lamassu-server --mockScoring --logLevel silly",
|
||||||
"jtest": "jest --detectOpenHandles",
|
"admin-server": "node --watch bin/lamassu-admin-server --dev --logLevel silly",
|
||||||
"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\"",
|
|
||||||
"stress-test": "cd tests/stress/ && node index.js 50 -v"
|
"stress-test": "cd tests/stress/ && node index.js 50 -v"
|
||||||
},
|
},
|
||||||
"nodemonConfig": {
|
|
||||||
"ignore": [
|
|
||||||
"new-lamassu-admin/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^5.3.0",
|
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,14 @@ setEnvVariable('KEY_PATH', `${process.env.PWD}/certs/Lamassu_OP.key`)
|
||||||
|
|
||||||
setEnvVariable(
|
setEnvVariable(
|
||||||
'MNEMONIC_PATH',
|
'MNEMONIC_PATH',
|
||||||
`${process.env.HOME}/.lamassu/mnemonics/mnemonic.txt`,
|
`${process.env.PWD}/.lamassu/mnemonics/mnemonic.txt`,
|
||||||
)
|
)
|
||||||
|
|
||||||
setEnvVariable('BLOCKCHAIN_DIR', `${process.env.PWD}/blockchains`)
|
setEnvVariable('BLOCKCHAIN_DIR', `${process.env.PWD}/blockchains`)
|
||||||
setEnvVariable('OFAC_DATA_DIR', `${process.env.HOME}/.lamassu/ofac`)
|
setEnvVariable('OFAC_DATA_DIR', `${process.env.PWD}/.lamassu/ofac`)
|
||||||
setEnvVariable('ID_PHOTO_CARD_DIR', `${process.env.HOME}/.lamassu/idphotocard`)
|
setEnvVariable('ID_PHOTO_CARD_DIR', `${process.env.PWD}/.lamassu/idphotocard`)
|
||||||
setEnvVariable('FRONT_CAMERA_DIR', `${process.env.HOME}/.lamassu/frontcamera`)
|
setEnvVariable('FRONT_CAMERA_DIR', `${process.env.PWD}/.lamassu/frontcamera`)
|
||||||
setEnvVariable('OPERATOR_DATA_DIR', `${process.env.HOME}/.lamassu/operatordata`)
|
setEnvVariable('OPERATOR_DATA_DIR', `${process.env.PWD}/.lamassu/operatordata`)
|
||||||
|
|
||||||
setEnvVariable('BTC_NODE_LOCATION', 'remote')
|
setEnvVariable('BTC_NODE_LOCATION', 'remote')
|
||||||
setEnvVariable('BTC_WALLET_LOCATION', 'local')
|
setEnvVariable('BTC_WALLET_LOCATION', 'local')
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ set -e
|
||||||
DOMAIN=localhost
|
DOMAIN=localhost
|
||||||
[ ! -z "$1" ] && DOMAIN=$1
|
[ ! -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
|
LOG_FILE=/tmp/cert-gen.log
|
||||||
CERT_DIR=$PWD/certs
|
CERT_DIR=$SERVER_DIR/certs
|
||||||
KEY_DIR=$PWD/certs
|
KEY_DIR=$SERVER_DIR/certs
|
||||||
LAMASSU_CA_PATH=$PWD/Lamassu_CA.pem
|
LAMASSU_CA_PATH=$SERVER_DIR/Lamassu_CA.pem
|
||||||
POSTGRES_PASS=postgres123
|
POSTGRES_PASS=postgres123
|
||||||
OFAC_DATA_DIR=$CONFIG_DIR/ofac
|
OFAC_DATA_DIR=$CONFIG_DIR/ofac
|
||||||
IDPHOTOCARD_DIR=$CONFIG_DIR/idphotocard
|
IDPHOTOCARD_DIR=$CONFIG_DIR/idphotocard
|
||||||
|
|
@ -24,7 +26,7 @@ MNEMONIC_DIR=$CONFIG_DIR/mnemonics
|
||||||
MNEMONIC_FILE=$MNEMONIC_DIR/mnemonic.txt
|
MNEMONIC_FILE=$MNEMONIC_DIR/mnemonic.txt
|
||||||
mkdir -p $MNEMONIC_DIR >> $LOG_FILE 2>&1
|
mkdir -p $MNEMONIC_DIR >> $LOG_FILE 2>&1
|
||||||
SEED=$(openssl rand -hex 32)
|
SEED=$(openssl rand -hex 32)
|
||||||
MNEMONIC=$($PWD/bin/bip39 $SEED)
|
MNEMONIC=$($SERVER_DIR/bin/bip39 $SEED)
|
||||||
echo "$MNEMONIC" > $MNEMONIC_FILE
|
echo "$MNEMONIC" > $MNEMONIC_FILE
|
||||||
|
|
||||||
echo "Generating SSL certificates..."
|
echo "Generating SSL certificates..."
|
||||||
|
|
@ -90,6 +92,6 @@ rm /tmp/Lamassu_OP.csr.pem
|
||||||
mkdir -p $OFAC_DATA_DIR/sources
|
mkdir -p $OFAC_DATA_DIR/sources
|
||||||
touch $OFAC_DATA_DIR/etags.json
|
touch $OFAC_DATA_DIR/etags.json
|
||||||
|
|
||||||
node tools/build-dev-env.js
|
(cd $SERVER_DIR && node tools/build-dev-env.js)
|
||||||
|
|
||||||
echo "Done."
|
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