Skip to content

Commit 793f816

Browse files
committed
Merge branch 'feat/collapse-payments-csv' of https://github.com/paybutton/paybutton-server into feat/collapse-payments-csv
2 parents d758d00 + b561946 commit 793f816

15 files changed

Lines changed: 221 additions & 38 deletions

File tree

constants/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const RESPONSE_MESSAGES = {
2424
TRANSACTION_ID_NOT_PROVIDED_400: { statusCode: 400, message: "'transactionId' not provided." },
2525
INVALID_NETWORK_SLUG_400: { statusCode: 400, message: 'Invalid network slug.' },
2626
INVALID_NETWORK_ID_400: { statusCode: 400, message: 'Invalid network id.' },
27+
INVALID_NETWORK_TICKER_400: { statusCode: 400, message: 'Invalid network ticker.' },
2728
INVALID_BUTTON_DATA_400: { statusCode: 400, message: "'buttonData' is not valid JSON." },
2829
INVALID_DATA_JSON_400: { statusCode: 400, message: 'Data is not valid JSON.' },
2930
PAYBUTTON_ALREADY_BELONGS_TO_WALLET_400: { statusCode: 400, message: 'One or more buttons already belong to another wallet.' },
@@ -46,7 +47,12 @@ export const RESPONSE_MESSAGES = {
4647
FAILED_TO_FETCH_PRICE_FROM_API_500: (day: string, ticker: string) => { return { statusCode: 500, message: `Failed to fetch ${ticker} price for day ${day}` } },
4748
MISSING_WS_AUTH_KEY_400: { statusCode: 400, message: 'Missing WS_AUTH_KEY environment variable' },
4849
MISSING_PRICE_FOR_TRANSACTION_400: { statusCode: 400, message: 'Missing price for transaction.' },
49-
INVALID_PRICES_FOR_TX_ON_CSV_CREATION_500: (pricesLenght: number) => { return {statusCode: 500, message: `Missing price for transaction in CSV creation. ${pricesLenght}` }},
50+
INVALID_PRICES_AMOUNT_FOR_TX_ON_CSV_CREATION_500: (pricesLenght: number) => {
51+
return {
52+
statusCode: 500,
53+
message: `Got wrong number of prices for transactions group in CSV creation. Expected 1, got ${pricesLenght}.`
54+
}
55+
},
5056
INVALID_PRICE_STATE_400: { statusCode: 400, message: 'Missing expected quote price for transaction.' },
5157
COULD_NOT_GET_BLOCK_INFO_500: { statusCode: 500, message: "Couldn't get block info." },
5258
NETWORK_SLUG_NOT_PROVIDED_400: { statusCode: 400, message: "'networkSlug' not provided." },

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"docker": "./scripts/docker-exec-shortcuts.sh",
1919
"ci:integration:test": "yarn pretest && dotenv -e .env.test -- ts-node -O '{\"module\":\"commonjs\"}' node_modules/jest/bin/jest.js tests/integration-tests --forceExit",
2020
"tarDebug": "tar cf debug.tar logs/ paybutton-config.json .env*",
21-
"updateAllPrices": "./scripts/update-all-prices.sh"
21+
"updateAllPrices": "./scripts/update-all-prices.sh",
22+
"updateAllPriceConnections": "./scripts/update-all-price-connections.sh"
2223
},
2324
"dependencies": {
2425
"@emotion/react": "^11.8.2",

pages/admin/index.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import Session from 'supertokens-node/recipe/session'
55
import { GetServerSideProps } from 'next'
66
import Page from 'components/Page'
77
import style from './admin.module.css'
8-
import { isUserAdmin, UserWithSupertokens } from 'services/userService'
8+
import { fetchUserWithSupertokens, isUserAdmin, UserWithSupertokens } from 'services/userService'
99
import { useRouter } from 'next/router'
1010
import RegisteredUsers from 'components/Admin/RegisteredUsers'
1111
import TableContainer from '../../components/TableContainer/TableContainer'
1212
import EyeIcon from 'assets/eye-icon.png'
1313
import Image from 'next/image'
14+
import { removeUnserializableFields } from 'utils'
1415

1516
export const getServerSideProps: GetServerSideProps = async (context) => {
1617
// this runs on the backend, so we must call init on supertokens-node SDK
@@ -29,7 +30,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
2930
}
3031
if (session === undefined) return
3132
const userId = session?.getUserId()
32-
const user = await supertokensNode.getUser(userId)
33+
const user = await fetchUserWithSupertokens(userId)
34+
removeUnserializableFields(user.userProfile)
35+
3336
const isAdmin = await isUserAdmin(userId)
3437
return {
3538
props: {
@@ -61,8 +64,8 @@ export default function Admin ({ user, isAdmin }: IProps): JSX.Element {
6164
useEffect(() => {
6265
void (async () => {
6366
const ok = await (await fetch('chronikStatus')).json()
64-
const subscribedEcashAddresses = ok.ecash.map((value: string) => ({ address: value }))
65-
const subscribedBitcoincashAddresses = ok.bitcoincash.map((value: string) => ({ address: value }))
67+
const subscribedEcashAddresses = ok.ecash?.map((value: string) => ({ address: value }))
68+
const subscribedBitcoincashAddresses = ok.bitcoincash?.map((value: string) => ({ address: value }))
6669
setEcashSubscribedAddresses(subscribedEcashAddresses)
6770
setBitcoincashSubscribedAddresses(subscribedBitcoincashAddresses)
6871
const ok2 = await (await fetch('/api/users')).json()
@@ -99,9 +102,9 @@ export default function Admin ({ user, isAdmin }: IProps): JSX.Element {
99102
<h2>Admin Dashboard</h2>
100103
<div className={style.admin_ctn}>
101104
<h3> eCash</h3>
102-
<TableContainer columns={columns} data={ecashSubscribedAddresses} ssr/>
105+
<TableContainer columns={columns} data={ecashSubscribedAddresses ?? []} ssr/>
103106
<h3> Bitcoin Cash</h3>
104-
<TableContainer columns={columns} data={bitcoincashSubscribedAddresses} ssr/>
107+
<TableContainer columns={columns} data={bitcoincashSubscribedAddresses ?? []} ssr/>
105108
<a
106109
target="_blank"
107110
rel="noopener noreferrer"

pages/api/chronikStatus/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getAllSubscribedAddresses } from 'services/chronikService'
1+
import { multiBlockchainClient } from 'services/chronikService'
22
import { fetchUserProfileFromId } from 'services/userService'
33
import { setSession } from 'utils/setSession'
44

@@ -11,7 +11,7 @@ export default async (req: any, res: any): Promise<void> => {
1111
if (user.isAdmin !== true) {
1212
throw new Error('unauthorised')
1313
}
14-
res.status(200).json(getAllSubscribedAddresses())
14+
res.status(200).json(multiBlockchainClient.getAllSubscribedAddresses())
1515
} catch (err: any) {
1616
switch (err.message) {
1717
default:

pages/api/paybutton/download/transactions/[paybuttonId].ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import {
33
NetworkTickersType,
44
NETWORK_IDS,
55
SUPPORTED_QUOTES_FROM_ID,
6-
NETWORK_SLUGS_FROM_IDS
6+
NETWORK_TICKERS
77
} from 'constants/index'
88
import { fetchTransactionsByPaybuttonId } from 'services/transactionService'
99
import { fetchPaybuttonById } from 'services/paybuttonService'
1010
import { isNetworkValid, downloadTxsFile } from 'utils/files'
1111
import { setSession } from 'utils/setSession'
12-
import { getNetworkIdFromSlug } from 'services/networkService'
1312
import { fetchUserProfileFromId } from 'services/userService'
1413

1514
export default async (req: any, res: any): Promise<void> => {
@@ -45,11 +44,10 @@ export default async (req: any, res: any): Promise<void> => {
4544
const timezone = userPreferredTimezone !== '' ? userPreferredTimezone : userReqTimezone
4645
let networkIdArray = Object.values(NETWORK_IDS)
4746
if (networkTicker !== undefined) {
48-
const slug = NETWORK_SLUGS_FROM_IDS[NETWORK_IDS[networkTicker]]
49-
if (slug === undefined) {
50-
throw new Error(RESPONSE_MESSAGES.INVALID_NETWORK_SLUG_400.message)
47+
if (!Object.values(NETWORK_TICKERS).includes(networkTicker)) {
48+
throw new Error(RESPONSE_MESSAGES.INVALID_NETWORK_TICKER_400.message)
5149
}
52-
const networkId = getNetworkIdFromSlug(slug)
50+
const networkId = NETWORK_IDS[networkTicker]
5351
networkIdArray = [networkId]
5452
};
5553
const transactions = await fetchTransactionsByPaybuttonId(paybutton.id, networkIdArray)

pages/api/paybutton/transactions/[id].ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { RESPONSE_MESSAGES } from 'constants/index'
22
import { fetchTransactionsByPaybuttonId } from 'services/transactionService'
33
import * as paybuttonService from 'services/paybuttonService'
44
import { setSession } from 'utils/setSession'
5+
import { parseError } from 'utils/validators'
56

67
export default async (req: any, res: any): Promise<void> => {
78
if (req.method === 'GET') {
@@ -19,14 +20,15 @@ export default async (req: any, res: any): Promise<void> => {
1920

2021
res.status(200).json({ transactions })
2122
} catch (err: any) {
22-
if (err.message === RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message) {
23+
const parsedError = parseError(err)
24+
if (parsedError.message === RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message) {
2325
res.status(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.statusCode)
2426
.json(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404)
25-
} else if (err.message === RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.message) {
27+
} else if (parsedError.message === RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.message) {
2628
res.status(RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.statusCode)
2729
.json(RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400)
2830
} else {
29-
res.status(500).json({ statusCode: 500, message: err.message })
31+
res.status(500).json({ statusCode: 500, message: parsedError.message })
3032
}
3133
}
3234
} else {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { fetchTxCountByPaybuttonId } from 'services/transactionService'
2+
import { RESPONSE_MESSAGES } from 'constants/index'
3+
import { fetchPaybuttonById } from 'services/paybuttonService'
4+
import { setSession } from 'utils/setSession'
5+
import { parseError } from 'utils/validators'
6+
7+
export default async (req: any, res: any): Promise<void> => {
8+
if (req.method === 'GET') {
9+
try {
10+
await setSession(req, res)
11+
const paybuttonId = req.query.id as string
12+
const userId = req.session.userId
13+
const paybutton = await fetchPaybuttonById(paybuttonId)
14+
if (paybutton.providerUserId !== userId) {
15+
throw new Error(RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.message)
16+
}
17+
const count = await fetchTxCountByPaybuttonId(paybuttonId)
18+
res.status(200).send(count)
19+
} catch (err: any) {
20+
const parsedError = parseError(err)
21+
switch (parsedError.message) {
22+
case RESPONSE_MESSAGES.NO_BUTTON_FOUND_404.message:
23+
res.status(404).json(RESPONSE_MESSAGES.NO_BUTTON_FOUND_404)
24+
break
25+
case RESPONSE_MESSAGES.USER_ID_NOT_PROVIDED_400.message:
26+
res.status(400).json(RESPONSE_MESSAGES.USER_ID_NOT_PROVIDED_400)
27+
break
28+
case RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400.message:
29+
res.status(400).json(RESPONSE_MESSAGES.RESOURCE_DOES_NOT_BELONG_TO_USER_400)
30+
break
31+
default:
32+
res.status(500).json({ statusCode: 500, message: err.message })
33+
}
34+
}
35+
}
36+
}

pages/api/payments/download/index.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import {
33
SUPPORTED_QUOTES_FROM_ID,
44
NetworkTickersType,
55
NETWORK_IDS,
6-
NETWORK_SLUGS_FROM_IDS
6+
NETWORK_TICKERS
77
} from 'constants/index'
88
import { downloadTxsFile, isNetworkValid } from 'utils/files'
99
import { setSession } from 'utils/setSession'
1010
import { fetchUserProfileFromId } from 'services/userService'
11-
import { getNetworkIdFromSlug } from 'services/networkService'
1211
import { fetchAllPaymentsByUserId } from 'services/transactionService'
1312

1413
export default async (req: any, res: any): Promise<void> => {
@@ -38,11 +37,10 @@ export default async (req: any, res: any): Promise<void> => {
3837
res.setHeader('Content-Type', 'text/csv')
3938
let networkIdArray = Object.values(NETWORK_IDS)
4039
if (networkTicker !== undefined) {
41-
const slug = NETWORK_SLUGS_FROM_IDS[NETWORK_IDS[networkTicker]]
42-
if (slug === undefined) {
43-
throw new Error(RESPONSE_MESSAGES.INVALID_NETWORK_SLUG_400.message)
40+
if (!Object.values(NETWORK_TICKERS).includes(networkTicker)) {
41+
throw new Error(RESPONSE_MESSAGES.INVALID_NETWORK_TICKER_400.message)
4442
}
45-
const networkId = getNetworkIdFromSlug(slug)
43+
const networkId = NETWORK_IDS[networkTicker]
4644
networkIdArray = [networkId]
4745
};
4846
const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
set -e
2+
3+
echo This will affect the database. Are you sure of what you\'re doing? [y/N]
4+
read ans
5+
if [[ "$ans" = "Y" || "$ans" = "y" ]]; then
6+
JOBS_ENV=true dotenv -e .env -c -- ts-node -O '{"module":"commonjs"}' -r tsconfig-paths/register ./scripts/updateAllPriceConnections.ts
7+
else
8+
echo Exited.
9+
fi
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { flattenTimestamp } from '../services/priceService'
2+
import prisma from 'prisma/clientInstance'
3+
import { connectTransactionsListToPrices } from 'services/transactionService'
4+
import { Transaction } from '@prisma/client'
5+
import moment from 'moment'
6+
import { exit } from 'process'
7+
8+
async function misconnectTxs (txsIds: string[]): Promise<void> {
9+
if (process.env.ENVIRONMENT === 'production') {
10+
return
11+
}
12+
if (txsIds.length === 0) {
13+
return
14+
}
15+
console.log('Misconnecting', txsIds.length, 'for testing purposes')
16+
for (const txId of txsIds) {
17+
const tx = await prisma.transaction.findUniqueOrThrow({
18+
where: {
19+
id: txId
20+
}
21+
})
22+
const txDayTimestamp = flattenTimestamp(tx.timestamp)
23+
let signal = 1
24+
if (txDayTimestamp === flattenTimestamp(moment.utc().unix())) {
25+
signal = -1
26+
}
27+
await prisma.transaction.update({
28+
where: {
29+
id: txId
30+
},
31+
data: {
32+
timestamp: tx.timestamp + (signal * 86400)
33+
}
34+
})
35+
}
36+
console.log('Finished misconnecting txs')
37+
}
38+
39+
async function fixMisconnectedTxs (): Promise<void> {
40+
const total = await prisma.transaction.count()
41+
const pageSize = 1000
42+
let page = 0
43+
44+
await misconnectTxs([
45+
// ADD TXS IDS HERE TO TEST IT
46+
])
47+
48+
console.log('Fixing misconnected txs...')
49+
while (true) {
50+
const txsToFix: Transaction[] = []
51+
// Get txs page
52+
const txs = await prisma.transaction.findMany({
53+
orderBy: {
54+
timestamp: 'asc'
55+
},
56+
include: {
57+
prices: {
58+
select: {
59+
price: {
60+
select: {
61+
timestamp: true
62+
}
63+
}
64+
}
65+
}
66+
},
67+
skip: page * pageSize,
68+
take: pageSize
69+
})
70+
71+
const viewedCount = page * pageSize + txs.length
72+
73+
// Finish if empty
74+
if (txs.length === 0) {
75+
break
76+
}
77+
78+
// Find txs with misaligned timestamps
79+
txs.forEach(tx => {
80+
const txFlattenedTimestamp = flattenTimestamp(tx.timestamp)
81+
const txPriceTimestamps = tx.prices.map(p => p.price.timestamp)
82+
if (txPriceTimestamps.filter(t => t !== txFlattenedTimestamp).length > 0) {
83+
txsToFix.push(tx)
84+
}
85+
})
86+
if (txsToFix.length !== 0) {
87+
console.log(`[${viewedCount}/${total}] Fixing ${txsToFix.length} txs...`)
88+
console.log('Tx ids:\n', txsToFix.map(t => t.id).join('\n'))
89+
await connectTransactionsListToPrices(txsToFix)
90+
console.log(`[${viewedCount}/${total}] Finished fixing ${txsToFix.length} txs.`)
91+
}
92+
console.log(`[${viewedCount}/${total}]`)
93+
page++
94+
}
95+
console.log('FINISHED')
96+
exit()
97+
}
98+
99+
async function run (): Promise<void> {
100+
await fixMisconnectedTxs()
101+
}
102+
103+
void run()

0 commit comments

Comments
 (0)