Skip to content

Commit 1fa8e81

Browse files
njlieoana-loleazeppelin44antoniu98
authored
feat(backend): publish webhooks to POS service if applicable (#3596)
* feat: created the backbone for the card service (#3508) * Created the backbone for the card service * Format fix * feat: add card service to docker compose --------- Co-authored-by: Nathan Lie <[email protected]> * feat: Integrate Redis client in Card Service for (requestId, posServiceHost) mapping (#3524) * Integrate Redis client in Card Service for (requestId, posServiceHost) mapping * remove unused dep * prettier fix * Separate logging params from the message itself * ttl * prettier fix * Rewrite redis service tests to use testcontainers instead of mocks --------- Co-authored-by: Antoniu Neacsu <[email protected]> * feat(point-of-sale): added route for registering a POS device (#3555) * Route for registering a POS device * Fixed issue addressed in comments, tried to fix the port issue of jest * Changed test container port to 0, format fix * feat(backend): publish webhooks to POS service if applicable * feat: add column to incoming payment * fix: tests * feat: add warn log * fix: better logger requirements for finalizing webhook recipients * fix: make open payments default reason for incoming payment initialization * fix: improve optional env var function * fix: warn log, better optional env, backfill migration, tests * fix: rebase bugs * chore: regenerate lockfile * chore: regenerate gql * fix: tests * fix: remove metadata; better tests * fix: build errors * fix: tests, add type file --------- Co-authored-by: oana-lolea <[email protected]> Co-authored-by: zeppelin44 <[email protected]> Co-authored-by: Antoniu Neacsu <[email protected]>
1 parent 058a0ed commit 1fa8e81

File tree

44 files changed

+694
-190
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+694
-190
lines changed

localenv/mock-account-servicing-entity/generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return knex.schema.alterTable('webhooks', function (table) {
7+
table.jsonb('metadata').nullable()
8+
})
9+
}
10+
11+
/**
12+
* @param { import("knex").Knex } knex
13+
* @returns { Promise<void> }
14+
*/
15+
exports.down = function (knex) {
16+
return knex.schema.alterTable('webhooks', function (table) {
17+
table.dropColumn('metadata')
18+
})
19+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return knex.schema
7+
.alterTable('incomingPayments', function (table) {
8+
table.enum('initiatedBy', ['CARD', 'OPEN_PAYMENTS', 'ADMIN'])
9+
})
10+
.then(() => {
11+
return Promise.all([
12+
knex.raw(
13+
`UPDATE "incomingPayments" SET "initiatedBy" = 'OPEN_PAYMENTS' WHERE "client" IS NOT NULL`
14+
),
15+
knex.raw(
16+
`UPDATE "incomingPayments" SET "initiatedBy" = 'ADMIN' WHERE "client" IS NULL`
17+
)
18+
])
19+
})
20+
.then(() => {
21+
return knex.raw(
22+
`ALTER TABLE "incomingPayments" ALTER COLUMN "initiatedBy" SET NOT NULL`
23+
)
24+
})
25+
}
26+
27+
/**
28+
* @param { import("knex").Knex } knex
29+
* @returns { Promise<void> }
30+
*/
31+
exports.down = function (knex) {
32+
return knex.schema.alterTable('incomingPayments', function (table) {
33+
table.dropColumn('initiatedBy')
34+
})
35+
}

packages/backend/src/config/app.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,20 @@ export const Config = {
203203
'SEND_TENANT_WEBHOOKS_TO_OPERATOR',
204204
false
205205
),
206-
cardServiceUrl: process.env.CARD_SERVICE_URL
206+
cardServiceUrl: optional(envString, 'CARD_SERVICE_URL'),
207+
posServiceUrl: optional(envString, 'POS_SERVICE_URL')
208+
}
209+
210+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
211+
function optional<T extends (...args: any[]) => ReturnType<T>>(
212+
envGetter: T,
213+
envVar: string
214+
): ReturnType<T> | undefined {
215+
try {
216+
return envGetter(envVar)
217+
} catch (err) {
218+
return undefined
219+
}
207220
}
208221

209222
function parseRedisTlsConfig(

packages/backend/src/graphql/generated/graphql.schema.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/graphql/generated/graphql.ts

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/graphql/resolvers/combined_payments.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from '../../open_payments/payment/combined/model'
2727
import { getPageTests } from './page.test'
2828
import { createTenant } from '../../tests/tenant'
29+
import { IncomingPaymentInitiationReason } from '../../open_payments/payment/incoming/types'
2930

3031
describe('Payment', (): void => {
3132
let deps: IocContract<AppServices>
@@ -84,7 +85,8 @@ describe('Payment', (): void => {
8485
const incomingPayment = await createIncomingPayment(deps, {
8586
walletAddressId: inWalletAddressId,
8687
client: client,
87-
tenantId: Config.operatorTenantId
88+
tenantId: Config.operatorTenantId,
89+
initiationReason: IncomingPaymentInitiationReason.OpenPayments
8890
})
8991

9092
const query = await appContainer.apolloClient

packages/backend/src/graphql/resolvers/incoming_payment.test.ts

Lines changed: 126 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { ApolloError, gql } from '@apollo/client'
22
import { getPageTests } from './page.test'
3-
import { createTestApp, TestContainer } from '../../tests/app'
3+
import {
4+
createApolloClient,
5+
createTestApp,
6+
TestContainer
7+
} from '../../tests/app'
48
import { IocContract } from '@adonisjs/fold'
59
import { AppServices } from '../../app'
610
import { initIocContainer } from '../..'
@@ -17,6 +21,7 @@ import {
1721
IncomingPayment as IncomingPaymentModel,
1822
IncomingPaymentState
1923
} from '../../open_payments/payment/incoming/model'
24+
import { IncomingPaymentInitiationReason } from '../../open_payments/payment/incoming/types'
2025
import {
2126
IncomingPayment,
2227
IncomingPaymentResponse,
@@ -29,6 +34,7 @@ import {
2934
} from '../../open_payments/payment/incoming/errors'
3035
import { Amount, serializeAmount } from '../../open_payments/amount'
3136
import { GraphQLErrorCode } from '../errors'
37+
import { createTenant } from '../../tests/tenant'
3238

3339
describe('Incoming Payment Resolver', (): void => {
3440
let deps: IocContract<AppServices>
@@ -81,7 +87,8 @@ describe('Incoming Payment Resolver', (): void => {
8187
description: `IncomingPayment`,
8288
externalRef: '#123'
8389
},
84-
tenantId
90+
tenantId,
91+
initiationReason: IncomingPaymentInitiationReason.Admin
8592
}),
8693
pagedQuery: 'incomingPayments',
8794
parent: {
@@ -122,7 +129,8 @@ describe('Incoming Payment Resolver', (): void => {
122129
metadata,
123130
expiresAt,
124131
incomingAmount,
125-
tenantId
132+
tenantId,
133+
initiationReason: IncomingPaymentInitiationReason.Admin
126134
})
127135

128136
const createSpy = jest
@@ -171,7 +179,11 @@ describe('Incoming Payment Resolver', (): void => {
171179
query.data?.createIncomingPayment
172180
)
173181

174-
expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId })
182+
expect(createSpy).toHaveBeenCalledWith({
183+
...input,
184+
tenantId,
185+
initiationReason: IncomingPaymentInitiationReason.Admin
186+
})
175187
expect(query).toEqual({
176188
__typename: 'IncomingPaymentResponse',
177189
payment: {
@@ -241,7 +253,11 @@ describe('Incoming Payment Resolver', (): void => {
241253
})
242254
)
243255
}
244-
expect(createSpy).toHaveBeenCalledWith({ ...input, tenantId })
256+
expect(createSpy).toHaveBeenCalledWith({
257+
...input,
258+
tenantId,
259+
initiationReason: IncomingPaymentInitiationReason.Admin
260+
})
245261
})
246262

247263
test('Internal server error', async (): Promise<void> => {
@@ -288,7 +304,107 @@ describe('Incoming Payment Resolver', (): void => {
288304
}
289305
expect(createSpy).toHaveBeenCalledWith({
290306
...input,
291-
tenantId
307+
tenantId,
308+
initiationReason: IncomingPaymentInitiationReason.Admin
309+
})
310+
})
311+
312+
describe('tenant boundaries', (): void => {
313+
test('operator can label incoming payment as card payment', async (): Promise<void> => {
314+
const createSpy = jest.spyOn(incomingPaymentService, 'create')
315+
316+
const input = {
317+
walletAddressId,
318+
isCardPayment: true
319+
}
320+
321+
const query = await appContainer.apolloClient
322+
.query({
323+
query: gql`
324+
mutation CreateIncomingPayment(
325+
$input: CreateIncomingPaymentInput!
326+
) {
327+
createIncomingPayment(input: $input) {
328+
payment {
329+
id
330+
walletAddressId
331+
}
332+
}
333+
}
334+
`,
335+
variables: { input }
336+
})
337+
.then(
338+
(query): IncomingPaymentResponse =>
339+
query.data?.createIncomingPayment
340+
)
341+
342+
expect(createSpy).toHaveBeenCalledWith({
343+
walletAddressId: input.walletAddressId,
344+
tenantId,
345+
initiationReason: IncomingPaymentInitiationReason.Card
346+
})
347+
expect(query).toEqual({
348+
__typename: 'IncomingPaymentResponse',
349+
payment: {
350+
__typename: 'IncomingPayment',
351+
id: expect.any(String),
352+
walletAddressId
353+
}
354+
})
355+
})
356+
357+
test('tenant cannot label incoming payment as card payment', async (): Promise<void> => {
358+
const tenant = await createTenant(deps)
359+
const tenantWalletAddress = await createWalletAddress(deps, {
360+
tenantId: tenant.id
361+
})
362+
const createSpy = jest.spyOn(incomingPaymentService, 'create')
363+
364+
const input = {
365+
walletAddressId: tenantWalletAddress.id,
366+
isCardPayment: true
367+
}
368+
369+
const tenantedApolloClient = await createApolloClient(
370+
appContainer.container,
371+
appContainer.app,
372+
tenant.id
373+
)
374+
const query = await tenantedApolloClient
375+
.query({
376+
query: gql`
377+
mutation CreateIncomingPayment(
378+
$input: CreateIncomingPaymentInput!
379+
) {
380+
createIncomingPayment(input: $input) {
381+
payment {
382+
id
383+
walletAddressId
384+
}
385+
}
386+
}
387+
`,
388+
variables: { input }
389+
})
390+
.then(
391+
(query): IncomingPaymentResponse =>
392+
query.data?.createIncomingPayment
393+
)
394+
395+
expect(createSpy).toHaveBeenCalledWith({
396+
walletAddressId: input.walletAddressId,
397+
tenantId: tenant.id,
398+
initiationReason: IncomingPaymentInitiationReason.Admin
399+
})
400+
expect(query).toEqual({
401+
__typename: 'IncomingPaymentResponse',
402+
payment: {
403+
__typename: 'IncomingPayment',
404+
id: expect.any(String),
405+
walletAddressId: tenantWalletAddress.id
406+
}
407+
})
292408
})
293409
})
294410
})
@@ -307,7 +423,8 @@ describe('Incoming Payment Resolver', (): void => {
307423
assetCode: asset.code,
308424
assetScale: asset.scale
309425
},
310-
tenantId
426+
tenantId,
427+
initiationReason: IncomingPaymentInitiationReason.Admin
311428
})
312429
}
313430

@@ -484,7 +601,8 @@ describe('Incoming Payment Resolver', (): void => {
484601
metadata,
485602
expiresAt,
486603
incomingAmount,
487-
tenantId
604+
tenantId,
605+
initiationReason: IncomingPaymentInitiationReason.Admin
488606
})
489607
const input = {
490608
id: payment.id,

packages/backend/src/graphql/resolvers/incoming_payment.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
QueryResolvers
77
} from '../generated/graphql'
88
import { IncomingPayment } from '../../open_payments/payment/incoming/model'
9+
import { IncomingPaymentInitiationReason } from '../../open_payments/payment/incoming/types'
910
import {
1011
isIncomingPaymentError,
1112
errorToCode,
@@ -97,14 +98,25 @@ export const createIncomingPayment: MutationResolvers<ForTenantIdContext>['creat
9798
throw new Error('Missing tenant id to create incoming payment')
9899
}
99100

101+
if (!ctx.isOperator && args.input.isCardPayment) {
102+
ctx.logger.warn(
103+
{ input: args.input, tenant: ctx.tenant },
104+
'non-operator cannot create card payment'
105+
)
106+
}
107+
100108
const incomingPaymentOrError = await incomingPaymentService.create({
101109
walletAddressId: args.input.walletAddressId,
102110
expiresAt: !args.input.expiresAt
103111
? undefined
104112
: new Date(args.input.expiresAt),
105113
incomingAmount: args.input.incomingAmount,
106114
metadata: args.input.metadata,
107-
tenantId
115+
tenantId,
116+
initiationReason:
117+
ctx.isOperator && args.input.isCardPayment
118+
? IncomingPaymentInitiationReason.Card
119+
: IncomingPaymentInitiationReason.Admin
108120
})
109121
if (isIncomingPaymentError(incomingPaymentOrError)) {
110122
throw new GraphQLError(errorToMessage[incomingPaymentOrError], {

0 commit comments

Comments
 (0)