Skip to content

Commit 741647d

Browse files
feat(stripe): stripe payments now create a transaction in wallet (#1986)
* feat(stripe): add transactions for stripe payments * fix(stripe): fix tests * chore(stripe): format
1 parent 2b3ed59 commit 741647d

6 files changed

Lines changed: 151 additions & 15 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @param { import("knex").Knex } knex
3+
* @returns { Promise<void> }
4+
*/
5+
exports.up = function (knex) {
6+
return knex.schema
7+
.table('transactions', (table) => {
8+
table.enum('source', ['Interledger', 'Stripe', 'Card'])
9+
})
10+
.then(() => {
11+
return knex('transactions')
12+
.update({ source: 'Card' })
13+
.where({ isCard: true })
14+
})
15+
.then(() => {
16+
return knex('transactions')
17+
.update({ source: 'Interledger' })
18+
.whereNull('source')
19+
})
20+
.then(() => {
21+
return knex.schema.table('transactions', (table) => {
22+
table.dropColumn('isCard')
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
33+
.table('transactions', (table) => {
34+
table.boolean('isCard').defaultTo(false)
35+
})
36+
.then(() => {
37+
return knex('transactions')
38+
.update({ isCard: true })
39+
.where({ source: 'Card' })
40+
})
41+
.then(() => {
42+
return knex.schema.table('transactions', (table) => {
43+
table.dropColumn('source')
44+
})
45+
})
46+
}

packages/wallet/backend/src/gatehub/service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class GateHubService {
166166
type: 'OUTGOING',
167167
status: 'COMPLETED',
168168
description: '',
169-
isCard: true
169+
source: 'Card'
170170
})
171171
}
172172

packages/wallet/backend/src/stripe-integration/service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { TransactionTypeEnum } from '../gatehub/consts'
66
import { StripeWebhookType } from './validation'
77
import { WalletAddressService } from '../walletAddress/service'
88
import { AccountService } from '../account/service'
9+
import { Transaction } from '../transaction/model'
10+
import { transformBalance } from '../utils/helpers'
911

1012
export enum EventType {
1113
payment_intent_canceled = 'payment_intent.canceled',
@@ -68,6 +70,18 @@ export class StripeService implements IStripeService {
6870
type: TransactionTypeEnum.HOSTED,
6971
message: 'Stripe Transfer'
7072
})
73+
74+
await Transaction.query().insert({
75+
walletAddressId: walletAddress.id,
76+
accountId: walletAddress.accountId,
77+
paymentId: paymentIntent.id,
78+
assetCode: currency.toUpperCase(),
79+
value: transformBalance(Number(amount), 2),
80+
type: 'INCOMING',
81+
status: 'COMPLETED',
82+
description: 'Stripe Payment',
83+
source: 'Stripe'
84+
})
7185
} catch (error) {
7286
this.logger.error('Error creating gatehub transaction', { error })
7387
throw new BadRequest('Failed to create transaction')

packages/wallet/backend/src/transaction/model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BaseModel } from '@shared/backend'
55
import { TransactionResponse } from '@wallet/shared'
66

77
export type TransactionType = 'INCOMING' | 'OUTGOING'
8+
export type TransactionSource = 'Interledger' | 'Stripe' | 'Card'
89
export type TransactionExtended = Transaction & {
910
walletAddressUrl: WalletAddress['url']
1011
accountName: Account['name']
@@ -31,7 +32,7 @@ export class Transaction
3132
assetCode!: string
3233
value!: bigint | null
3334
walletAddress!: WalletAddress
34-
isCard?: boolean
35+
source!: TransactionSource
3536
txAmount?: bigint
3637
txCurrency?: string
3738
conversionRate?: string

packages/wallet/backend/src/transaction/service.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Transaction } from './model'
1+
import { Transaction, TransactionSource } from './model'
22
import { OrderByDirection, Page, PartialModelObject } from 'objection'
33
import { AccountService } from '@/account/service'
44
import { Logger } from 'winston'
@@ -122,7 +122,7 @@ export class TransactionService implements ITransactionService {
122122
}
123123

124124
const latestTransaction: Transaction | undefined = await Transaction.query()
125-
.findOne({ accountId: account.id, isCard: true })
125+
.findOne({ accountId: account.id, source: 'Card' })
126126
.orderBy('createdAt', 'DESC')
127127

128128
const walletAddress = await WalletAddress.query().findOne({
@@ -171,7 +171,7 @@ export class TransactionService implements ITransactionService {
171171
type: 'OUTGOING',
172172
status: 'COMPLETED',
173173
description: '',
174-
isCard: true,
174+
source: 'Card' as TransactionSource,
175175
secondParty: transaction.merchantName,
176176
txAmount: transaction.transactionAmount
177177
? transformBalance(Number(transaction.transactionAmount), 2)
@@ -229,7 +229,8 @@ export class TransactionService implements ITransactionService {
229229
value: amount.value,
230230
type: 'INCOMING',
231231
status: 'PENDING',
232-
description: params.metadata?.description
232+
description: params.metadata?.description,
233+
source: 'Interledger'
233234
})
234235
}
235236

@@ -256,7 +257,8 @@ export class TransactionService implements ITransactionService {
256257
status: 'PENDING',
257258
description: params.metadata?.description,
258259
secondParty: secondParty?.names,
259-
secondPartyWA: secondParty?.walletAddresses
260+
secondPartyWA: secondParty?.walletAddresses,
261+
source: 'Interledger'
260262
})
261263
}
262264
}

packages/wallet/backend/tests/stripe-integration/service.test.ts

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ import { StripeService, EventType } from '@/stripe-integration/service'
99
import { GateHubClient } from '@/gatehub/client'
1010
import { TransactionTypeEnum } from '@/gatehub/consts'
1111
import { StripeWebhookType } from '../../src/stripe-integration/validation'
12+
import { Transaction } from '@/transaction/model'
13+
import { Account } from '@/account/model'
14+
import { WalletAddress } from '@/walletAddress/model'
15+
import { loginUser } from '@/tests/utils'
16+
import { mockedListAssets } from '@/tests/mocks'
1217

1318
describe('Stripe Service', (): void => {
1419
let bindings: AwilixContainer<Cradle>
1520
let appContainer: TestApp
1621
let knex: Knex
1722
let stripeService: StripeService
23+
let userId: string
24+
let account: Account
25+
let walletAddress: WalletAddress
26+
let walletId: string
1827

1928
const mockGateHubClient = {
2029
createTransaction: jest.fn(),
@@ -78,6 +87,33 @@ describe('Stripe Service', (): void => {
7887
})
7988

8089
beforeEach(async (): Promise<void> => {
90+
// Necessary db setup for transaction creation
91+
const { user } = await loginUser({
92+
authService: await bindings.resolve('authService'),
93+
extraUserArgs: {
94+
isEmailVerified: true,
95+
gateHubUserId: 'test-gatehub-user'
96+
}
97+
})
98+
userId = user.id
99+
walletId = faker.string.uuid()
100+
101+
account = await Account.query().insert({
102+
name: faker.string.alpha(10),
103+
userId: userId,
104+
assetCode: 'USD',
105+
assetId: mockedListAssets[0].id,
106+
assetScale: 2,
107+
gateHubWalletId: 'gatehub-wallet-123'
108+
})
109+
110+
walletAddress = await WalletAddress.query().insert({
111+
url: 'wallet_address_123',
112+
publicName: 'Test Wallet',
113+
accountId: account.id,
114+
id: walletId
115+
})
116+
81117
Reflect.set(
82118
stripeService,
83119
'gateHubClient',
@@ -87,19 +123,12 @@ describe('Stripe Service', (): void => {
87123
Reflect.set(stripeService, 'walletAddressService', mockWalletAddressService)
88124
Reflect.set(stripeService, 'accountService', mockAccountService)
89125

90-
// Mock vault UUID lookup
91126
mockGateHubClient.getVaultUuid.mockReturnValue('vault-uuid-123')
92127

93-
// Mock successful transaction creation
94128
mockGateHubClient.createTransaction.mockResolvedValue(undefined)
95129

96-
// Mock wallet address lookup
97-
mockWalletAddressService.getByUrl.mockResolvedValue({
98-
id: 'wallet-123',
99-
url: 'wallet_address_123'
100-
})
130+
mockWalletAddressService.getByUrl.mockResolvedValue(walletAddress)
101131

102-
// Mock gatehub wallet address lookup
103132
mockAccountService.getGateHubWalletAddress.mockResolvedValue({
104133
gateHubWalletId: 'gatehub-wallet-123'
105134
})
@@ -119,6 +148,19 @@ describe('Stripe Service', (): void => {
119148
type: TransactionTypeEnum.HOSTED,
120149
message: 'Stripe Transfer'
121150
})
151+
152+
const transactions = await Transaction.query()
153+
expect(transactions).toHaveLength(1)
154+
expect(transactions[0]).toMatchObject({
155+
walletAddressId: walletAddress.id,
156+
accountId: account.id,
157+
paymentId: webhook.data.object.id,
158+
assetCode: webhook.data.object.currency.toUpperCase(),
159+
type: 'INCOMING',
160+
status: 'COMPLETED',
161+
description: 'Stripe Payment',
162+
source: 'Stripe'
163+
})
122164
})
123165

124166
it('should handle payment_intent_payment_failed event type', async (): Promise<void> => {
@@ -135,6 +177,9 @@ describe('Stripe Service', (): void => {
135177
})
136178
)
137179
expect(mockGateHubClient.createTransaction).not.toHaveBeenCalled()
180+
181+
const transactions = await Transaction.query()
182+
expect(transactions).toHaveLength(0)
138183
})
139184

140185
it('should handle payment_intent_canceled event type', async (): Promise<void> => {
@@ -150,6 +195,9 @@ describe('Stripe Service', (): void => {
150195
})
151196
)
152197
expect(mockGateHubClient.createTransaction).not.toHaveBeenCalled()
198+
199+
const transactions = await Transaction.query()
200+
expect(transactions).toHaveLength(0)
153201
})
154202

155203
it('should log information about the received webhook', async (): Promise<void> => {
@@ -162,6 +210,9 @@ describe('Stripe Service', (): void => {
162210
`received webhook of type : ${webhook.type} for : ${webhook.id}`
163211
)
164212
)
213+
214+
const transactions = await Transaction.query()
215+
expect(transactions).toHaveLength(1)
165216
})
166217
})
167218

@@ -182,6 +233,19 @@ describe('Stripe Service', (): void => {
182233
type: TransactionTypeEnum.HOSTED,
183234
message: 'Stripe Transfer'
184235
})
236+
237+
const transactions = await Transaction.query()
238+
expect(transactions).toHaveLength(1)
239+
expect(transactions[0]).toMatchObject({
240+
walletAddressId: walletAddress.id,
241+
accountId: account.id,
242+
paymentId: webhook.data.object.id,
243+
assetCode: webhook.data.object.currency.toUpperCase(),
244+
type: 'INCOMING',
245+
status: 'COMPLETED',
246+
description: 'Stripe Payment',
247+
source: 'Stripe'
248+
})
185249
})
186250

187251
it('should throw error when GateHub transaction creation fails', async (): Promise<void> => {
@@ -204,6 +268,9 @@ describe('Stripe Service', (): void => {
204268
error: expect.any(Error)
205269
})
206270
)
271+
272+
const transactions = await Transaction.query()
273+
expect(transactions).toHaveLength(0)
207274
})
208275
})
209276

@@ -224,6 +291,9 @@ describe('Stripe Service', (): void => {
224291
error: webhook.data.object.last_payment_error
225292
})
226293
)
294+
295+
const transactions = await Transaction.query()
296+
expect(transactions).toHaveLength(0)
227297
})
228298
})
229299

@@ -243,6 +313,9 @@ describe('Stripe Service', (): void => {
243313
receiving_address: webhook.data.object.metadata.receiving_address
244314
})
245315
)
316+
317+
const transactions = await Transaction.query()
318+
expect(transactions).toHaveLength(0)
246319
})
247320
})
248321
})

0 commit comments

Comments
 (0)