diff --git a/packages/shared/backend/src/model.ts b/packages/shared/backend/src/model.ts index 3198326c0..f5c4b52b3 100644 --- a/packages/shared/backend/src/model.ts +++ b/packages/shared/backend/src/model.ts @@ -16,7 +16,9 @@ export abstract class BaseModel extends Model { public $beforeInsert(context: QueryContext): void { super.$beforeInsert(context) - this.createdAt = new Date() + if (!this.createdAt) { + this.createdAt = new Date() + } this.updatedAt = new Date() } diff --git a/packages/wallet/backend/src/card/service.ts b/packages/wallet/backend/src/card/service.ts index dece516d6..62d4ad32c 100644 --- a/packages/wallet/backend/src/card/service.ts +++ b/packages/wallet/backend/src/card/service.ts @@ -80,8 +80,14 @@ export class CardService { pageNumber?: number ): Promise { await this.ensureAccountExists(userId, cardId) + const { gateHubUserId } = await this.ensureGatehubUserUuid(userId) - return this.gateHubClient.getCardTransactions(cardId, pageSize, pageNumber) + return this.gateHubClient.getCardTransactions( + cardId, + gateHubUserId, + pageSize, + pageNumber + ) } async getCardLimits( diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index d00bf83ea..1383f5108 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -511,10 +511,11 @@ export class GateHubClient { async getCardTransactions( cardId: string, + userUuid: string, pageSize?: number, pageNumber?: number ): Promise { - let url = `${this.apiUrl}/v1/cards/${cardId}/transactions` + let url = `${this.apiUrl}/cards/v1/cards/${cardId}/transactions` const queryParams: string[] = [] @@ -529,7 +530,10 @@ export class GateHubClient { url += `?${queryParams.join('&')}` } - return this.request('GET', url) + return this.request('GET', url, undefined, { + cardAppId: this.env.GATEHUB_CARD_APP_ID, + managedUserUuid: userUuid + }) } async getCardLimits(cardId: string): Promise { diff --git a/packages/wallet/backend/src/transaction/service.ts b/packages/wallet/backend/src/transaction/service.ts index 63ea8a6a8..67b95316f 100644 --- a/packages/wallet/backend/src/transaction/service.ts +++ b/packages/wallet/backend/src/transaction/service.ts @@ -3,14 +3,18 @@ import { OrderByDirection, Page, PartialModelObject } from 'objection' import { AccountService } from '@/account/service' import { Logger } from 'winston' import { PaginationQueryParams } from '@/shared/types' -import { prefixSomeObjectKeys } from '@/utils/helpers' +import { prefixSomeObjectKeys, transformBalance } from '@/utils/helpers' import { Knex } from 'knex' import { IncomingPayment, OutgoingPayment } from '@/rafiki/backend/generated/graphql' import { WalletAddress } from '@/walletAddress/model' +import { Account } from '@/account/model' +import { CardService } from '@/card/service' +import NodeCache from 'node-cache' +const FETCHING_TRANSACTIONS_KEY = 'FETCHING_TRANSACTIONS' type ListAllTransactionsInput = { userId: string paginationParams: PaginationQueryParams @@ -34,10 +38,12 @@ export interface ITransactionService { } export class TransactionService implements ITransactionService { + cache: NodeCache = new NodeCache({ stdTTL: 30 }) constructor( private accountService: AccountService, private logger: Logger, - private knex: Knex + private knex: Knex, + private cardService: CardService ) {} async list( @@ -71,6 +77,10 @@ export class TransactionService implements ITransactionService { filterParams, orderByDate }: ListAllTransactionsInput): Promise> { + if (page === 0) { + await this.fetchCardTransactions(userId) + } + const filterParamsWithTableNames = prefixSomeObjectKeys( filterParams, ['walletAddressId', 'assetCode', 'type', 'status', 'accountId'], @@ -95,6 +105,75 @@ export class TransactionService implements ITransactionService { return transactions } + async fetchCardTransactions(userId: string) { + const key = `${FETCHING_TRANSACTIONS_KEY}-${userId}` + if (this.cache.has(key)) { + return + } + this.cache.set(key, true) + + const account = await Account.query().findOne({ userId, assetCode: 'EUR' }) + if (!account?.cardId) { + return + } + + const latestTransaction: Transaction | undefined = await Transaction.query() + .findOne({ accountId: account.id, isCard: true }) + .orderBy('createdAt', 'DESC') + + const walletAddress = await WalletAddress.query().findOne({ + accountId: account.id, + isCard: true + }) + + if (!walletAddress) { + return + } + + let page = 1 + const pageSize = 10 + let shouldFetchNext = true + while (shouldFetchNext) { + const transactionsResponse = await this.cardService.getCardTransactions( + userId, + account.cardId, + pageSize, + page + ) + + if (transactionsResponse.data.length === 0) { + return + } + + const newTransactions = transactionsResponse.data.filter( + (transaction) => + !latestTransaction || + latestTransaction.createdAt.toISOString() <= transaction.createdAt + ) + if (transactionsResponse.data.length > newTransactions.length) { + shouldFetchNext = false + } + page++ + + const transactionsToSave: Partial[] = newTransactions.map( + (transaction) => ({ + walletAddressId: walletAddress.id, + accountId: walletAddress.accountId, + paymentId: transaction.transactionId, + assetCode: transaction.billingCurrency, + value: transformBalance(Number(transaction.billingAmount), 2), + type: 'OUTGOING', + status: 'COMPLETED', + description: '', + isCard: true, + createdAt: new Date(transaction.createdAt) + }) + ) + + await Transaction.query().insert(transactionsToSave) + } + } + async processPendingIncomingPayments(): Promise { return this.knex.transaction(async (trx) => { // Giving a Rafiki a little more time to process the payments before we process them.