diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Accounting Ledger Transfers.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Accounting Ledger Transfers.bru index e1322e309d..b9b2686b72 100644 --- a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Accounting Ledger Transfers.bru +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Accounting Ledger Transfers.bru @@ -21,6 +21,8 @@ body:graphql { transferType ledger createdAt + state + expiresAt } credits { id @@ -30,6 +32,8 @@ body:graphql { transferType ledger createdAt + state + expiresAt } } } diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 9437d6ec24..46dad2dedb 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -32,10 +32,14 @@ export type AccountingTransfer = Model & { creditAccountId: Scalars['ID']['output']; /** Unique identifier for the debit account. */ debitAccountId: Scalars['ID']['output']; + /** The date and time that the accounting transfer will expire. */ + expiresAt?: Maybe; /** Unique identifier for the accounting transfer. */ id: Scalars['ID']['output']; /** Identifier that partitions the sets of accounts that can transact with each other. */ ledger: Scalars['UInt8']['output']; + /** The state of the accounting transfer. */ + state: TransferState; /** Type of the accounting transfer. */ transferType: TransferType; }; @@ -1372,6 +1376,15 @@ export enum SortOrder { Desc = 'DESC' } +export enum TransferState { + /** The accounting transfer is pending */ + Pending = 'PENDING', + /** The accounting transfer is posted */ + Posted = 'POSTED', + /** The accounting transfer is voided */ + Voided = 'VOIDED' +} + export enum TransferType { /** Represents a deposit transfer. */ Deposit = 'DEPOSIT', @@ -1812,6 +1825,7 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1966,8 +1980,10 @@ export type AccountingTransferResolvers; creditAccountId?: Resolver; debitAccountId?: Resolver; + expiresAt?: Resolver, ParentType, ContextType>; id?: Resolver; ledger?: Resolver; + state?: Resolver; transferType?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/accounting/tigerbeetle/service.test.ts b/packages/backend/src/accounting/tigerbeetle/service.test.ts index 7d38242d8a..686f77ea21 100644 --- a/packages/backend/src/accounting/tigerbeetle/service.test.ts +++ b/packages/backend/src/accounting/tigerbeetle/service.test.ts @@ -235,6 +235,41 @@ describe('TigerBeetle Accounting Service', (): void => { expect(transferDebit.expiresAt).toBeUndefined() }) + test('Returns expiry date correctly', async (): Promise => { + const receivingAccount = await accountFactory.build() + const balances = [BigInt(100), BigInt(100)] + const accounts = await Promise.all( + balances.map(async (balance) => { + return await accountFactory.build({ + balance, + asset: receivingAccount.asset + }) + }) + ) + const timeout = 10 + await Promise.all( + accounts.map(async (account, i) => { + const transfer = await accountingService.createTransfer({ + sourceAccount: account, + sourceAmount: BigInt(10 * (i + 1)), + destinationAccount: receivingAccount, + timeout: timeout + }) + assert.ok(!isTransferError(transfer)) + await transfer.post() + }) + ) + const transfer = await accountingService.getAccountTransfers( + receivingAccount.id + ) + expect(transfer.credits).not.toHaveLength(0) + const timestamp = transfer.credits[0].timestamp + const expiresAtTime = transfer.credits[0].expiresAt?.getTime() + expect(expiresAtTime).toBe( + new Date(Number(timestamp) + timeout * 1000).getTime() + ) + }) + test('Returns undefined for nonexistent account', async (): Promise => { await expect( accountingService.getTotalReceived(uuid()) diff --git a/packages/backend/src/accounting/tigerbeetle/utils.ts b/packages/backend/src/accounting/tigerbeetle/utils.ts index 3553e306f9..9817b2f453 100644 --- a/packages/backend/src/accounting/tigerbeetle/utils.ts +++ b/packages/backend/src/accounting/tigerbeetle/utils.ts @@ -60,6 +60,19 @@ export function tbTransferToLedgerTransfer( transferRef: fromTigerBeetleId(tbTransfer.user_data_128), type: transferTypeFromCode(tbTransfer.code), state: state, - ledger: tbTransfer.ledger + ledger: tbTransfer.ledger, + expiresAt: expiresAtFromTimestampAndTimeout( + tbTransfer.timestamp, + tbTransfer.timeout + ) } } + +function expiresAtFromTimestampAndTimeout( + timestamp: bigint, + timeout: number +): Date | undefined { + return timeout + ? new Date(Number(timestamp / 1_000_000n) + timeout * 1000) + : undefined +} diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index a4fa9fb362..1745966aa2 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -77,6 +77,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "expiresAt", + "description": "The date and time that the accounting transfer will expire.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "id", "description": "Unique identifier for the accounting transfer.", @@ -109,6 +121,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "state", + "description": "The state of the accounting transfer.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TransferState", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "transferType", "description": "Type of the accounting transfer.", @@ -7596,6 +7624,35 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "TransferState", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "PENDING", + "description": "The accounting transfer is pending", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "POSTED", + "description": "The accounting transfer is posted", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VOIDED", + "description": "The accounting transfer is voided", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "ENUM", "name": "TransferType", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 9437d6ec24..46dad2dedb 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -32,10 +32,14 @@ export type AccountingTransfer = Model & { creditAccountId: Scalars['ID']['output']; /** Unique identifier for the debit account. */ debitAccountId: Scalars['ID']['output']; + /** The date and time that the accounting transfer will expire. */ + expiresAt?: Maybe; /** Unique identifier for the accounting transfer. */ id: Scalars['ID']['output']; /** Identifier that partitions the sets of accounts that can transact with each other. */ ledger: Scalars['UInt8']['output']; + /** The state of the accounting transfer. */ + state: TransferState; /** Type of the accounting transfer. */ transferType: TransferType; }; @@ -1372,6 +1376,15 @@ export enum SortOrder { Desc = 'DESC' } +export enum TransferState { + /** The accounting transfer is pending */ + Pending = 'PENDING', + /** The accounting transfer is posted */ + Posted = 'POSTED', + /** The accounting transfer is voided */ + Voided = 'VOIDED' +} + export enum TransferType { /** Represents a deposit transfer. */ Deposit = 'DEPOSIT', @@ -1812,6 +1825,7 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1966,8 +1980,10 @@ export type AccountingTransferResolvers; creditAccountId?: Resolver; debitAccountId?: Resolver; + expiresAt?: Resolver, ParentType, ContextType>; id?: Resolver; ledger?: Resolver; + state?: Resolver; transferType?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts b/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts index 6be3b65e3c..1ec2f06a61 100644 --- a/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts +++ b/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts @@ -7,6 +7,7 @@ import { Config } from '../../config/app' import { truncateTables } from '../../tests/tableManager' import { AccountingTransferConnection, + TransferState, TransferType } from '../generated/graphql' import { createAsset } from '../../tests/asset' @@ -99,6 +100,7 @@ describe('Accounting Transfer', (): void => { // Top up debit account first: const transferAmount = 123 const ledger = 1 + const tomorrow = new Date(new Date().getDate() + 1) const queryLimit = 20 const insertedTransfer = await createLedgerTransfer( @@ -109,7 +111,8 @@ describe('Accounting Transfer', (): void => { ledger: ledger, state: LedgerTransferState.POSTED, transferRef: uuid(), - type: LedgerTransferType.DEPOSIT + type: LedgerTransferType.DEPOSIT, + expiresAt: tomorrow }, appContainer.knex ) @@ -127,6 +130,8 @@ describe('Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } credits { id @@ -136,6 +141,8 @@ describe('Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } } } @@ -161,7 +168,9 @@ describe('Accounting Transfer', (): void => { debitAccountId: accountDebitId, amount: `${transferAmount}`, transferType: TransferType.Deposit, - ledger + ledger, + state: TransferState.Posted, + expiresAt: tomorrow.toISOString() }) // Credit: @@ -179,6 +188,8 @@ describe('Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } credits { id @@ -188,6 +199,8 @@ describe('Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } } } @@ -214,7 +227,9 @@ describe('Accounting Transfer', (): void => { creditAccountId: accountCreditId, amount: `${transferAmount}`, transferType: TransferType.Deposit, - ledger + ledger, + state: TransferState.Posted, + expiresAt: tomorrow.toISOString() }) }) }) diff --git a/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts b/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts index 94d7e81540..b6fe08cee7 100644 --- a/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts +++ b/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts @@ -11,6 +11,7 @@ import { Config } from '../../config/app' import { truncateTables } from '../../tests/tableManager' import { AccountingTransferConnection, + TransferState, TransferType } from '../generated/graphql' import { v4 as uuid } from 'uuid' @@ -95,6 +96,8 @@ describe('TigerBeetle: Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } credits { id @@ -104,6 +107,8 @@ describe('TigerBeetle: Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } } } @@ -129,7 +134,9 @@ describe('TigerBeetle: Accounting Transfer', (): void => { creditAccountId: creditAccId, amount: `${transferAmount}`, transferType: TransferType.Deposit, - ledger + ledger, + state: TransferState.Posted, + expiresAt: null }) // Credit: @@ -146,6 +153,8 @@ describe('TigerBeetle: Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } credits { id @@ -155,6 +164,8 @@ describe('TigerBeetle: Accounting Transfer', (): void => { transferType ledger createdAt + state + expiresAt } } } @@ -180,7 +191,9 @@ describe('TigerBeetle: Accounting Transfer', (): void => { creditAccountId: creditAccId, amount: `${transferAmount}`, transferType: TransferType.Deposit, - ledger + ledger, + state: TransferState.Posted, + expiresAt: null }) }) }) diff --git a/packages/backend/src/graphql/resolvers/accounting_transfer.ts b/packages/backend/src/graphql/resolvers/accounting_transfer.ts index c19db8a700..c278b78527 100644 --- a/packages/backend/src/graphql/resolvers/accounting_transfer.ts +++ b/packages/backend/src/graphql/resolvers/accounting_transfer.ts @@ -2,10 +2,15 @@ import { ResolversTypes, QueryResolvers, AccountingTransfer, - TransferType as SchemaTransferType + TransferType as SchemaTransferType, + TransferState } from '../generated/graphql' import { ApolloContext } from '../../app' -import { LedgerTransfer, TransferType } from '../../accounting/service' +import { + LedgerTransfer, + LedgerTransferState, + TransferType +} from '../../accounting/service' export const getAccountingTransfers: QueryResolvers['accountingTransfers'] = async ( @@ -37,7 +42,11 @@ export function transferToGraphql( creditAccountId: transfer.creditAccountId, amount: transfer.amount, ledger: transfer.ledger, - transferType: transferTypeToGraphql(transfer.type) + transferType: transferTypeToGraphql(transfer.type), + state: transferStateToGraphql(transfer.state), + expiresAt: transfer.expiresAt + ? new Date(transfer.expiresAt).toISOString() + : undefined } } @@ -53,3 +62,16 @@ function transferTypeToGraphql(type: TransferType): SchemaTransferType { throw new Error(`Transfer type '${type}' is not mapped!`) } } + +function transferStateToGraphql(state: LedgerTransferState): TransferState { + switch (state) { + case LedgerTransferState.PENDING: + return TransferState.Pending + case LedgerTransferState.POSTED: + return TransferState.Posted + case LedgerTransferState.VOIDED: + return TransferState.Voided + default: + throw new Error(`Transfer state '${state}' is not mapped!`) + } +} diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index ef622b214d..f8286e14d4 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1062,6 +1062,19 @@ type AccountingTransfer implements Model { ledger: UInt8! "The date and time that the accounting transfer was created." createdAt: String! + "The state of the accounting transfer." + state: TransferState! + "The date and time that the accounting transfer will expire." + expiresAt: String +} + +enum TransferState { + "The accounting transfer is pending" + PENDING + "The accounting transfer is posted" + POSTED + "The accounting transfer is voided" + VOIDED } enum TransferType { diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 4bc2279d6f..1116595860 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -32,10 +32,14 @@ export type AccountingTransfer = Model & { creditAccountId: Scalars['ID']['output']; /** Unique identifier for the debit account. */ debitAccountId: Scalars['ID']['output']; + /** The date and time that the accounting transfer will expire. */ + expiresAt?: Maybe; /** Unique identifier for the accounting transfer. */ id: Scalars['ID']['output']; /** Identifier that partitions the sets of accounts that can transact with each other. */ ledger: Scalars['UInt8']['output']; + /** The state of the accounting transfer. */ + state: TransferState; /** Type of the accounting transfer. */ transferType: TransferType; }; @@ -1372,6 +1376,15 @@ export enum SortOrder { Desc = 'DESC' } +export enum TransferState { + /** The accounting transfer is pending */ + Pending = 'PENDING', + /** The accounting transfer is posted */ + Posted = 'POSTED', + /** The accounting transfer is voided */ + Voided = 'VOIDED' +} + export enum TransferType { /** Represents a deposit transfer. */ Deposit = 'DEPOSIT', @@ -1812,6 +1825,7 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1966,8 +1980,10 @@ export type AccountingTransferResolvers; creditAccountId?: Resolver; debitAccountId?: Resolver; + expiresAt?: Resolver, ParentType, ContextType>; id?: Resolver; ledger?: Resolver; + state?: Resolver; transferType?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 9437d6ec24..46dad2dedb 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -32,10 +32,14 @@ export type AccountingTransfer = Model & { creditAccountId: Scalars['ID']['output']; /** Unique identifier for the debit account. */ debitAccountId: Scalars['ID']['output']; + /** The date and time that the accounting transfer will expire. */ + expiresAt?: Maybe; /** Unique identifier for the accounting transfer. */ id: Scalars['ID']['output']; /** Identifier that partitions the sets of accounts that can transact with each other. */ ledger: Scalars['UInt8']['output']; + /** The state of the accounting transfer. */ + state: TransferState; /** Type of the accounting transfer. */ transferType: TransferType; }; @@ -1372,6 +1376,15 @@ export enum SortOrder { Desc = 'DESC' } +export enum TransferState { + /** The accounting transfer is pending */ + Pending = 'PENDING', + /** The accounting transfer is posted */ + Posted = 'POSTED', + /** The accounting transfer is voided */ + Voided = 'VOIDED' +} + export enum TransferType { /** Represents a deposit transfer. */ Deposit = 'DEPOSIT', @@ -1812,6 +1825,7 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1966,8 +1980,10 @@ export type AccountingTransferResolvers; creditAccountId?: Resolver; debitAccountId?: Resolver; + expiresAt?: Resolver, ParentType, ContextType>; id?: Resolver; ledger?: Resolver; + state?: Resolver; transferType?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 9437d6ec24..46dad2dedb 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -32,10 +32,14 @@ export type AccountingTransfer = Model & { creditAccountId: Scalars['ID']['output']; /** Unique identifier for the debit account. */ debitAccountId: Scalars['ID']['output']; + /** The date and time that the accounting transfer will expire. */ + expiresAt?: Maybe; /** Unique identifier for the accounting transfer. */ id: Scalars['ID']['output']; /** Identifier that partitions the sets of accounts that can transact with each other. */ ledger: Scalars['UInt8']['output']; + /** The state of the accounting transfer. */ + state: TransferState; /** Type of the accounting transfer. */ transferType: TransferType; }; @@ -1372,6 +1376,15 @@ export enum SortOrder { Desc = 'DESC' } +export enum TransferState { + /** The accounting transfer is pending */ + Pending = 'PENDING', + /** The accounting transfer is posted */ + Posted = 'POSTED', + /** The accounting transfer is voided */ + Voided = 'VOIDED' +} + export enum TransferType { /** Represents a deposit transfer. */ Deposit = 'DEPOSIT', @@ -1812,6 +1825,7 @@ export type ResolversTypes = { SetFeeResponse: ResolverTypeWrapper>; SortOrder: ResolverTypeWrapper>; String: ResolverTypeWrapper>; + TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; TriggerWalletAddressEventsInput: ResolverTypeWrapper>; TriggerWalletAddressEventsMutationResponse: ResolverTypeWrapper>; @@ -1966,8 +1980,10 @@ export type AccountingTransferResolvers; creditAccountId?: Resolver; debitAccountId?: Resolver; + expiresAt?: Resolver, ParentType, ContextType>; id?: Resolver; ledger?: Resolver; + state?: Resolver; transferType?: Resolver; __isTypeOf?: IsTypeOfResolverFn; };