From 7d191f23fc8d4c3490a726461e768aa451941b98 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Wed, 13 Aug 2025 13:51:52 +0300 Subject: [PATCH 1/5] Made CARD_SERVICE_URL optional --- packages/backend/src/config/app.ts | 2 +- packages/backend/src/open_payments/wallet_address/model.ts | 4 ++-- packages/backend/src/open_payments/wallet_address/routes.ts | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 344f18c4e3..853ed84d4e 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -203,7 +203,7 @@ export const Config = { 'SEND_TENANT_WEBHOOKS_TO_OPERATOR', false ), - cardServiceUrl: envString('CARD_SERVICE_URL') + cardServiceUrl: process.env.CARD_SERVICE_URL } function parseRedisTlsConfig( diff --git a/packages/backend/src/open_payments/wallet_address/model.ts b/packages/backend/src/open_payments/wallet_address/model.ts index d5beb73af3..299ba1c31f 100644 --- a/packages/backend/src/open_payments/wallet_address/model.ts +++ b/packages/backend/src/open_payments/wallet_address/model.ts @@ -123,7 +123,7 @@ export class WalletAddress }: { authServer: string resourceServer: string - cardService: string + cardService?: string }): OpenPaymentsWalletAddress { const returnVal: OpenPaymentsWalletAddress = { id: this.address, @@ -132,7 +132,7 @@ export class WalletAddress assetScale: this.asset.scale, authServer, resourceServer, - cardService + ...(cardService && { cardService }) } if (this.additionalProperties && this.additionalProperties.length) { returnVal.additionalProperties = this.additionalProperties diff --git a/packages/backend/src/open_payments/wallet_address/routes.ts b/packages/backend/src/open_payments/wallet_address/routes.ts index a718af835b..267d87f3dd 100644 --- a/packages/backend/src/open_payments/wallet_address/routes.ts +++ b/packages/backend/src/open_payments/wallet_address/routes.ts @@ -63,7 +63,9 @@ export async function getWalletAddress( ctx.body = walletAddress.toOpenPaymentsType({ authServer: `${ensureTrailingSlash(deps.config.authServerGrantUrl)}${walletAddress.tenantId}`, resourceServer: `${ensureTrailingSlash(deps.config.openPaymentsUrl)}${walletAddress.tenantId}`, - cardService: `${ensureTrailingSlash(deps.config.cardServiceUrl)}${walletAddress.tenantId}` + ...(deps.config.cardServiceUrl && { + cardService: `${ensureTrailingSlash(deps.config.cardServiceUrl)}${walletAddress.tenantId}` + }) }) } From c418642249bc139f84e0ba9dc00ae3553eb106ac Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Wed, 13 Aug 2025 14:16:30 +0300 Subject: [PATCH 2/5] Created cardService container only if cardServiceUrl is set in config --- packages/backend/src/index.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 01bc9d374f..c25c7b66b6 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -672,13 +672,16 @@ export function initIocContainer( }) }) - container.singleton('cardService', async (deps) => { - return createCardService({ - axios: await deps.use('axios'), - logger: await deps.use('logger'), - cardServiceUrl: config.cardServiceUrl + if (config.cardServiceUrl) { + const cardServiceUrl = config.cardServiceUrl + container.singleton('cardService', async (deps) => { + return createCardService({ + axios: await deps.use('axios'), + logger: await deps.use('logger'), + cardServiceUrl + }) }) - }) + } return container } From 96d13b844c0362344799cab20b51367bcbb82440 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Thu, 14 Aug 2025 13:04:18 +0300 Subject: [PATCH 3/5] Added noop card service when cardServiceUrl is not provided --- packages/backend/src/card/service.ts | 8 ++++++++ packages/backend/src/index.ts | 21 ++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/card/service.ts b/packages/backend/src/card/service.ts index 7e43781899..73ce39ce1c 100644 --- a/packages/backend/src/card/service.ts +++ b/packages/backend/src/card/service.ts @@ -36,6 +36,14 @@ export async function createCardService( } } +export async function createNoopCardService(): Promise { + return { + async sendPaymentEvent(_eventDetails) { + // do nothing + } + } +} + async function sendPaymentEvent( deps: ServiceDependencies, eventDetails: EventDetails diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index c25c7b66b6..2f2448bcd8 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -77,7 +77,7 @@ import { createTenantService } from './tenants/service' import { AuthServiceClient } from './auth-service-client/client' import { createTenantSettingService } from './tenants/settings/service' import { createPaymentMethodProviderService } from './payment-method/provider/service' -import { createCardService } from './card/service' +import { createCardService, createNoopCardService } from './card/service' BigInt.prototype.toJSON = function () { return this.toString() @@ -672,16 +672,15 @@ export function initIocContainer( }) }) - if (config.cardServiceUrl) { - const cardServiceUrl = config.cardServiceUrl - container.singleton('cardService', async (deps) => { - return createCardService({ - axios: await deps.use('axios'), - logger: await deps.use('logger'), - cardServiceUrl - }) - }) - } + container.singleton('cardService', async (deps) => { + return config.cardServiceUrl + ? createCardService({ + axios: await deps.use('axios'), + logger: await deps.use('logger'), + cardServiceUrl: config.cardServiceUrl + }) + : createNoopCardService() + }) return container } From 43da82b35c8eaf0b0941ab08a73046de4d09dbe4 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Thu, 14 Aug 2025 13:18:21 +0300 Subject: [PATCH 4/5] Regenerated graphql --- packages/frontend/app/generated/graphql.ts | 4 +- .../src/graphql/generated/graphql.ts | 61 ++++++++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index cb661f9c18..ded2783b8e 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -3063,7 +3063,7 @@ export type ListTenantsQueryVariables = Exact<{ }>; -export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, settings: Array<{ __typename?: 'TenantSetting', key: string, value: string }> } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; +export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, settings: Array<{ __typename?: 'TenantSetting', key: TenantSettingKey, value: string }> } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; export type CreateTenantMutationVariables = Exact<{ input: CreateTenantInput; @@ -3091,7 +3091,7 @@ export type GetTenantQueryVariables = Exact<{ }>; -export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, settings: Array<{ __typename?: 'TenantSetting', key: string, value: string }> } }; +export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, settings: Array<{ __typename?: 'TenantSetting', key: TenantSettingKey, value: string }> } }; export type GetWalletAddressQueryVariables = Exact<{ id: Scalars['String']['input']; diff --git a/packages/point-of-sale/src/graphql/generated/graphql.ts b/packages/point-of-sale/src/graphql/generated/graphql.ts index 4b8d3a754b..083be05076 100644 --- a/packages/point-of-sale/src/graphql/generated/graphql.ts +++ b/packages/point-of-sale/src/graphql/generated/graphql.ts @@ -191,6 +191,13 @@ export type CancelOutgoingPaymentInput = { reason?: InputMaybe; }; +export type CardDetailsInput = { + /** Expire date */ + expiry: Scalars['String']['input']; + /** Signature */ + signature: Scalars['String']['input']; +}; + export type CreateAssetInput = { /** Should be an ISO 4217 currency code whenever possible, e.g. `USD`. For more information, refer to [assets](https://rafiki.dev/overview/concepts/accounting/#assets). */ code: Scalars['String']['input']; @@ -278,6 +285,8 @@ export type CreateOutgoingPaymentFromIncomingPaymentInput = { }; export type CreateOutgoingPaymentInput = { + /** Used for the card service to provide the card expiry and signature */ + cardDetails?: InputMaybe; /** Unique key to ensure duplicate or retried requests are processed only once. For more information, refer to [idempotency](https://rafiki.dev/apis/graphql/admin-api-overview/#idempotency). */ idempotencyKey?: InputMaybe; /** Additional metadata associated with the outgoing payment. */ @@ -1006,6 +1015,8 @@ export type MutationWithdrawEventLiquidityArgs = { export type OutgoingPayment = BasePayment & Model & { __typename?: 'OutgoingPayment'; + /** Used for the card service to provide the card expiry and signature */ + cardDetails?: Maybe; /** Information about the wallet address of the Open Payments client that created the outgoing payment. */ client?: Maybe; /** The date and time that the outgoing payment was created. */ @@ -1040,6 +1051,16 @@ export type OutgoingPayment = BasePayment & Model & { walletAddressId: Scalars['ID']['output']; }; +export type OutgoingPaymentCardDetails = Model & { + __typename?: 'OutgoingPaymentCardDetails'; + createdAt: Scalars['String']['output']; + expiry: Scalars['String']['output']; + id: Scalars['ID']['output']; + outgoingPaymentId: Scalars['ID']['output']; + signature: Scalars['String']['output']; + updatedAt: Scalars['String']['output']; +}; + export type OutgoingPaymentConnection = { __typename?: 'OutgoingPaymentConnection'; /** A list of outgoing payment edges, containing outgoing payment nodes and cursors for pagination. */ @@ -1511,18 +1532,27 @@ export type TenantMutationResponse = { export type TenantSetting = { __typename?: 'TenantSetting'; /** Key for this setting. */ - key: Scalars['String']['output']; + key: TenantSettingKey; /** Value of a setting for this key. */ value: Scalars['String']['output']; }; export type TenantSettingInput = { /** Key for this setting. */ - key: Scalars['String']['input']; + key: TenantSettingKey; /** Value of a setting for this key. */ value: Scalars['String']['input']; }; +export enum TenantSettingKey { + ExchangeRatesUrl = 'EXCHANGE_RATES_URL', + IlpAddress = 'ILP_ADDRESS', + WalletAddressUrl = 'WALLET_ADDRESS_URL', + WebhookMaxRetry = 'WEBHOOK_MAX_RETRY', + WebhookTimeout = 'WEBHOOK_TIMEOUT', + WebhookUrl = 'WEBHOOK_URL' +} + export type TenantsConnection = { __typename?: 'TenantsConnection'; /** A list of edges representing tenants and cursors for pagination. */ @@ -1898,7 +1928,7 @@ export type DirectiveResolverFn> = { BasePayment: ( Partial ) | ( Partial ) | ( Partial ); - Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); + Model: ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ) | ( Partial ); }; /** Mapping between all available schema types and the resolvers types */ @@ -1921,6 +1951,7 @@ export type ResolversTypes = { CancelIncomingPaymentInput: ResolverTypeWrapper>; CancelIncomingPaymentResponse: ResolverTypeWrapper>; CancelOutgoingPaymentInput: ResolverTypeWrapper>; + CardDetailsInput: ResolverTypeWrapper>; CreateAssetInput: ResolverTypeWrapper>; CreateAssetLiquidityWithdrawalInput: ResolverTypeWrapper>; CreateIncomingPaymentInput: ResolverTypeWrapper>; @@ -1982,6 +2013,7 @@ export type ResolversTypes = { Model: ResolverTypeWrapper['Model']>; Mutation: ResolverTypeWrapper<{}>; OutgoingPayment: ResolverTypeWrapper>; + OutgoingPaymentCardDetails: ResolverTypeWrapper>; OutgoingPaymentConnection: ResolverTypeWrapper>; OutgoingPaymentEdge: ResolverTypeWrapper>; OutgoingPaymentFilter: ResolverTypeWrapper>; @@ -2014,6 +2046,7 @@ export type ResolversTypes = { TenantMutationResponse: ResolverTypeWrapper>; TenantSetting: ResolverTypeWrapper>; TenantSettingInput: ResolverTypeWrapper>; + TenantSettingKey: ResolverTypeWrapper>; TenantsConnection: ResolverTypeWrapper>; TransferState: ResolverTypeWrapper>; TransferType: ResolverTypeWrapper>; @@ -2065,6 +2098,7 @@ export type ResolversParentTypes = { CancelIncomingPaymentInput: Partial; CancelIncomingPaymentResponse: Partial; CancelOutgoingPaymentInput: Partial; + CardDetailsInput: Partial; CreateAssetInput: Partial; CreateAssetLiquidityWithdrawalInput: Partial; CreateIncomingPaymentInput: Partial; @@ -2121,6 +2155,7 @@ export type ResolversParentTypes = { Model: ResolversInterfaceTypes['Model']; Mutation: {}; OutgoingPayment: Partial; + OutgoingPaymentCardDetails: Partial; OutgoingPaymentConnection: Partial; OutgoingPaymentEdge: Partial; OutgoingPaymentFilter: Partial; @@ -2392,7 +2427,7 @@ export type LiquidityMutationResponseResolvers = { - __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; + __resolveType: TypeResolveFn<'AccountingTransfer' | 'Asset' | 'Fee' | 'IncomingPayment' | 'OutgoingPayment' | 'OutgoingPaymentCardDetails' | 'Payment' | 'Peer' | 'Tenant' | 'WalletAddress' | 'WalletAddressKey' | 'WebhookEvent', ParentType, ContextType>; createdAt?: Resolver; id?: Resolver; }; @@ -2439,6 +2474,7 @@ export type MutationResolvers = { + cardDetails?: Resolver, ParentType, ContextType>; client?: Resolver, ParentType, ContextType>; createdAt?: Resolver; debitAmount?: Resolver; @@ -2458,6 +2494,16 @@ export type OutgoingPaymentResolvers; }; +export type OutgoingPaymentCardDetailsResolvers = { + createdAt?: Resolver; + expiry?: Resolver; + id?: Resolver; + outgoingPaymentId?: Resolver; + signature?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type OutgoingPaymentConnectionResolvers = { edges?: Resolver, ParentType, ContextType>; pageInfo?: Resolver; @@ -2633,7 +2679,7 @@ export type TenantMutationResponseResolvers = { - key?: Resolver; + key?: Resolver; value?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -2792,6 +2838,7 @@ export type Resolvers = { Model?: ModelResolvers; Mutation?: MutationResolvers; OutgoingPayment?: OutgoingPaymentResolvers; + OutgoingPaymentCardDetails?: OutgoingPaymentCardDetailsResolvers; OutgoingPaymentConnection?: OutgoingPaymentConnectionResolvers; OutgoingPaymentEdge?: OutgoingPaymentEdgeResolvers; OutgoingPaymentResponse?: OutgoingPaymentResponseResolvers; @@ -3016,7 +3063,7 @@ export type ListTenantsQueryVariables = Exact<{ }>; -export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; +export type ListTenantsQuery = { __typename?: 'Query', tenants: { __typename?: 'TenantsConnection', edges: Array<{ __typename?: 'TenantEdge', node: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, settings: Array<{ __typename?: 'TenantSetting', key: TenantSettingKey, value: string }> } }>, pageInfo: { __typename?: 'PageInfo', startCursor?: string | null, endCursor?: string | null, hasNextPage: boolean, hasPreviousPage: boolean } } }; export type CreateTenantMutationVariables = Exact<{ input: CreateTenantInput; @@ -3044,7 +3091,7 @@ export type GetTenantQueryVariables = Exact<{ }>; -export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null } }; +export type GetTenantQuery = { __typename?: 'Query', tenant: { __typename?: 'Tenant', id: string, email?: string | null, apiSecret: string, idpConsentUrl?: string | null, idpSecret?: string | null, publicName?: string | null, createdAt: string, deletedAt?: string | null, settings: Array<{ __typename?: 'TenantSetting', key: TenantSettingKey, value: string }> } }; export type GetWalletAddressQueryVariables = Exact<{ id: Scalars['String']['input']; From c01679417f687712fc5ed5b1648764e5e0e387a3 Mon Sep 17 00:00:00 2001 From: Oana Lolea Date: Thu, 14 Aug 2025 20:08:32 +0300 Subject: [PATCH 5/5] Added warning when CARD_SERVICE_URL is not set --- packages/backend/src/card/service.ts | 7 ++++++- packages/backend/src/index.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/card/service.ts b/packages/backend/src/card/service.ts index 73ce39ce1c..b2c6399fca 100644 --- a/packages/backend/src/card/service.ts +++ b/packages/backend/src/card/service.ts @@ -36,10 +36,15 @@ export async function createCardService( } } -export async function createNoopCardService(): Promise { +export async function createNoopCardService( + logger: Logger +): Promise { return { async sendPaymentEvent(_eventDetails) { // do nothing + logger.warn( + 'CARD_SERVICE_URL env variable not set, falling back to NoopCardService' + ) } } } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 2f2448bcd8..9076f626a4 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -679,7 +679,7 @@ export function initIocContainer( logger: await deps.use('logger'), cardServiceUrl: config.cardServiceUrl }) - : createNoopCardService() + : createNoopCardService(await deps.use('logger')) }) return container