Skip to content

Conversation

sanducb
Copy link
Contributor

@sanducb sanducb commented Sep 17, 2025

Changes proposed in this pull request

This PR adds new webhook events for outgoing payments.
In order not to have the POS service waiting until the incoming payment expires, we have to reply to the POS service request with the ASEs decision on the outgoing payment. This is done by having backend send new webhooks to the card service: outgoing_payment.funded and outgoing_payment.cancelled.

Context

Closes RAF-1156

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Make sure that all checks pass
  • Bruno collection updated (if necessary)
  • Documentation issue created with user-docs label (if necessary)
  • OpenAPI specs updated (if necessary)

@github-actions github-actions bot added type: tests Testing related pkg: backend Changes in the backend package. pkg: frontend Changes in the frontend package. type: source Changes business logic pkg: mock-ase pkg: mock-account-service-lib labels Sep 17, 2025
Copy link

github-actions bot commented Sep 17, 2025

🚀 Performance Test Results

Test Configuration:

  • VUs: 4
  • Duration: 1m0s

Test Metrics:

  • Requests/s: 42.58
  • Iterations/s: 14.21
  • Failed Requests: 0.00% (0 of 2562)
📜 Logs

> [email protected] run-tests:testenv /home/runner/work/rafiki/rafiki/test/performance
> ./scripts/run-tests.sh -e test "-k" "-q" "--vus" "4" "--duration" "1m"

Cloud Nine GraphQL API is up: http://localhost:3101/graphql
Cloud Nine Wallet Address is up: http://localhost:3100/
Happy Life Bank Address is up: http://localhost:4100/
cloud-nine-wallet-test-backend already set
cloud-nine-wallet-test-auth already set
happy-life-bank-test-backend already set
happy-life-bank-test-auth already set
     data_received..................: 925 kB 15 kB/s
     data_sent......................: 2.0 MB 33 kB/s
     http_req_blocked...............: avg=6.76µs   min=2.54µs  med=5.59µs   max=550.14µs p(90)=6.63µs   p(95)=7.09µs  
     http_req_connecting............: avg=522ns    min=0s      med=0s       max=510.94µs p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=93.31ms  min=6.99ms  med=76.78ms  max=566.3ms  p(90)=163.77ms p(95)=186.41ms
       { expected_response:true }...: avg=93.31ms  min=6.99ms  med=76.78ms  max=566.3ms  p(90)=163.77ms p(95)=186.41ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 2562
     http_req_receiving.............: avg=89.32µs  min=21.06µs med=78.37µs  max=2.41ms   p(90)=117.91µs p(95)=146.58µs
     http_req_sending...............: avg=36.03µs  min=11.63µs med=28.98µs  max=1.81ms   p(90)=41.72µs  p(95)=57.88µs 
     http_req_tls_handshaking.......: avg=0s       min=0s      med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=93.18ms  min=6.83ms  med=76.65ms  max=566.18ms p(90)=163.68ms p(95)=186.27ms
     http_reqs......................: 2562   42.578174/s
     iteration_duration.............: avg=281.25ms min=174.5ms med=268.69ms max=1.16s    p(90)=348.87ms p(95)=377.25ms
     iterations.....................: 855    14.209344/s
     vus............................: 4      min=4       max=4 
     vus_max........................: 4      min=4       max=4 

@sanducb sanducb changed the title feat(backend): additional webhook events for outgoing payments feat(backend): [wip] additional webhook events for outgoing payments Sep 17, 2025
@sanducb sanducb changed the title feat(backend): [wip] additional webhook events for outgoing payments feat(backend): additional webhook events for outgoing payments Sep 17, 2025
await knex.schema.alterTable(
'outgoingPaymentCardDetails',
function (table) {
table.string('requestId').nullable()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this non-nullable

import { LiquidityAccount } from '../../../accounting/service'
import { Asset } from '../../../asset/model'
import { ConnectorAccount } from '../../../payment-method/ilp/connector/core/rafiki'
import { OutgoingPaymentInitiationReason } from './types'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, but we can keep OutgoingPaymentInitiationReason here

Comment on lines 343 to 345
if (initiationReason === IncomingPaymentInitiationReason.Card) {
recipients = recipients.concat(buildPosRecipient())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new outgoing payment events should be sent only to card service, and not POS service (outgoing payments -> card service, incoming payments -> POS Service).

I think we should also make this function take in an object instead of a list of params for readability

// So default the amountSent and balance to 0 for outgoing payments still in the funding or cancelled states
const isZeroAmountSent =
payment.state === OutgoingPaymentState.Funding ||
type === OutgoingPaymentEventType.PaymentCancelled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type === OutgoingPaymentEventType.PaymentCancelled
payment.state === OutgoingPaymentState.Cancelled

@sanducb sanducb requested a review from mkurapov September 22, 2025 09:04
): Promise<Asset> {
if (this.liquidityThreshold !== null) {
if (balance <= this.liquidityThreshold) {
const { finalizeWebhookRecipients } = await import('../webhook/service')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the dynamic import here?

Copy link
Contributor Author

@sanducb sanducb Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it because IncomingPaymentInitiationReason was causing a cycle in the static imports after I moved it to model.ts. (I guess this is why it was in a different file before). So in order to be able to properly use this enum inside webhook service and still have the type in model.ts I had to break the cycle by adding this dynamic import. We can of course keep it in a separate file, but as you suggested here for the outgoing payment equivalent enum, it is cleaner this way. Also, we can hardcode the enum variant (e.g. "CARD") here instead of using the type, but I think that might create confusion on the long run.

payment.walletAddressId
)

if (payment.initiatedBy === OutgoingPaymentInitiationReason.Card) {
Copy link
Contributor

@mkurapov mkurapov Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm debating whether we keep this conditional, or still store the webhookEvent for cancelled & funded, but just don't send it to anyone if it was done for a non-card payment. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first thought is that it is confusing to store webhook events that will not be sent, especially if doing so by calling sendWebhookEvent here. Is there any reason we would need to store these events (e.g. audit, proofs)? If yes, then I think we should find a way to store this information separately, without touching webhooks logic.

Cancelled = 'CANCELLED'
}

export enum OutgoingPaymentDepositType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, neither the cancelled nor the funded events are "deposit" or "funded" type. We can either check whether the OutgoingPaymentWithdrawType or OutgoingPaymentDepositType are used anywhere and if they are, just make a third category for the cancelled or funded.

return await query.getPage(pagination, sortOrder)
}

type FinalizeRecipientsOptions = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having sendToPosService and sendToCardService parameters, and just let the caller decide when those are true? Then this function doesn't need to know about anything payments related

@sanducb sanducb merged commit 6a457f4 into pos-card-services Sep 23, 2025
28 of 40 checks passed
@sanducb sanducb deleted the bs/raf-1156 branch September 23, 2025 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: backend Changes in the backend package. pkg: frontend Changes in the frontend package. pkg: mock-account-service-lib pkg: mock-ase type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants