Skip to content

Commit 5d74df6

Browse files
committed
refactor: improve PaymentService and TransactionsController with DI and better SoC
1 parent 95e4564 commit 5d74df6

2 files changed

Lines changed: 117 additions & 63 deletions

File tree

app/controllers/transactions_controller.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { type HttpContext } from '@adonisjs/core/http'
2+
import { inject } from '@adonisjs/core'
23
import Transaction from '#models/transaction'
34
import Product from '#models/product'
45
import { PaymentService } from '#services/payment_service'
56
import vine from '@vinejs/vine'
67

8+
@inject()
79
export default class TransactionsController {
8-
private paymentService = new PaymentService()
10+
constructor(private paymentService: PaymentService) {}
911

1012
public async purchase({ request, response }: HttpContext) {
1113
const schema = vine.compile(
@@ -31,21 +33,11 @@ export default class TransactionsController {
3133

3234
const payload = await request.validateUsing(schema)
3335

34-
const items = await Promise.all(
35-
payload.products.map(async (p) => {
36-
const product = await Product.findOrFail(p.id)
37-
return { id: product.id, quantity: p.quantity, price: product.amount }
38-
})
39-
)
40-
41-
const total = items.reduce((acc, item) => acc + item.price * item.quantity, 0)
42-
4336
try {
44-
const transaction = await this.paymentService.processPayment(
45-
payload.client,
46-
{ ...payload.payment, products: items },
47-
total
48-
)
37+
const transaction = await this.paymentService.processPayment(payload.client, {
38+
...payload.payment,
39+
products: payload.products,
40+
})
4941

5042
return response.ok(transaction)
5143
} catch (e) {

app/services/payment_service.ts

Lines changed: 110 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,162 @@
1+
import { inject } from '@adonisjs/core'
12
import Gateway from '#models/gateway'
23
import Transaction from '#models/transaction'
34
import Client from '#models/client'
5+
import Product from '#models/product'
46
import { GatewayOne } from './gateways/gateway_one.js'
57
import { GatewayTwo } from './gateways/gateway_two.js'
68
import { type PaymentGateway, type PaymentRequest } from './gateways/payment_gateway.js'
79

10+
@inject()
811
export class PaymentService {
9-
private gateways: Record<string, PaymentGateway> = {
12+
private gatewayInstances: Record<string, PaymentGateway> = {
1013
'Gateway 1': new GatewayOne(),
1114
'Gateway 2': new GatewayTwo(),
1215
}
1316

17+
/**
18+
* Orchestrates the payment process across multiple gateways with fallback support.
19+
*/
1420
public async processPayment(
1521
customer: { name: string; email: string },
16-
payment: { cardNumber: string; cvv: string; products: { id: number; quantity: number }[] },
17-
amount: number
22+
payment: { cardNumber: string; cvv: string; products: { id: number; quantity: number }[] }
1823
) {
19-
const client = await Client.firstOrCreate({ email: customer.email }, { name: customer.name })
20-
21-
const providers = await Gateway.query().where('isActive', true).orderBy('priority', 'asc')
24+
const amount = await this.calculateTotal(payment.products)
25+
const client = await this.getOrCreateClient(customer)
26+
const activeGateways = await this.getActiveGatewaysSortedByPriority()
2227

23-
if (!providers.length) {
24-
throw new Error('Nenhum gateway de pagamento configurado ou ativo')
28+
if (!activeGateways.length) {
29+
throw new Error('No payment gateways configured or active')
2530
}
2631

27-
const transaction = await Transaction.create({
28-
clientId: client.id,
29-
amount,
30-
status: 'PENDING',
31-
cardLastNumbers: payment.cardNumber.slice(-4),
32-
})
32+
const transaction = await this.createPendingTransaction(client.id, amount, payment.cardNumber)
33+
await this.attachProductsToTransaction(transaction, payment.products)
3334

34-
for (const p of payment.products) {
35-
await transaction.related('products').attach({ [p.id]: { quantity: p.quantity } })
36-
}
37-
38-
const request: PaymentRequest = {
39-
amount,
40-
name: client.name,
41-
email: client.email,
42-
cardNumber: payment.cardNumber,
43-
cvv: payment.cvv,
44-
}
35+
const paymentRequest = this.buildPaymentRequest(client, payment, amount)
4536

4637
let success = false
47-
let errorLog = ''
38+
let lastError = 'Unknown error'
4839

49-
for (const model of providers) {
50-
const instance = this.gateways[model.name]
40+
for (const gatewayModel of activeGateways) {
41+
const instance = this.gatewayInstances[gatewayModel.name]
5142
if (!instance) continue
5243

53-
const res = await instance.pay(request)
44+
const result = await instance.pay(paymentRequest)
5445

55-
if (res.success) {
56-
transaction.merge({
57-
status: 'PAID',
58-
gatewayId: model.id,
59-
externalId: res.externalId,
60-
})
61-
await transaction.save()
46+
if (result.success) {
47+
await this.finalizeTransaction(transaction, gatewayModel.id, result.externalId!)
6248
success = true
6349
break
6450
}
6551

66-
errorLog = res.error || 'Erro desconhecido'
52+
lastError = result.error || 'Payment failed'
6753
}
6854

6955
if (!success) {
70-
transaction.status = 'FAILED'
71-
await transaction.save()
72-
throw new Error(`Falha no processamento: ${errorLog}`)
56+
await this.markTransactionAsFailed(transaction)
57+
throw new Error(`Payment processing failed: ${lastError}`)
7358
}
7459

7560
return transaction
7661
}
7762

78-
public async refund(id: number) {
79-
const transaction = await Transaction.query().where('id', id).preload('gateway').firstOrFail()
63+
/**
64+
* Calculates the total amount for a list of products.
65+
*/
66+
public async calculateTotal(items: { id: number; quantity: number }[]): Promise<number> {
67+
const products = await Promise.all(
68+
items.map(async (item) => {
69+
const product = await Product.findOrFail(item.id)
70+
return product.amount * item.quantity
71+
})
72+
)
73+
return products.reduce((acc, current) => acc + current, 0)
74+
}
75+
76+
/**
77+
* Refunds a paid transaction using the gateway that processed it.
78+
*/
79+
public async refund(transactionId: number) {
80+
const transaction = await Transaction.query()
81+
.where('id', transactionId)
82+
.preload('gateway')
83+
.firstOrFail()
8084

8185
if (transaction.status !== 'PAID') {
82-
throw new Error('Apenas transações pagas podem ser reembolsadas')
86+
throw new Error('Only paid transactions can be refunded')
8387
}
8488

85-
const instance = this.gateways[transaction.gateway?.name || '']
89+
const instance = this.gatewayInstances[transaction.gateway?.name || '']
8690
if (!instance) {
87-
throw new Error('Implementação do gateway não encontrada para estorno')
91+
throw new Error('Gateway implementation not found for refund')
8892
}
8993

90-
const res = await instance.refund(transaction.externalId!)
94+
const result = await instance.refund(transaction.externalId!)
9195

92-
if (res.success) {
96+
if (result.success) {
9397
transaction.status = 'REFUNDED'
9498
await transaction.save()
9599
return true
96100
}
97101

98-
throw new Error(`Falha no estorno: ${res.error}`)
102+
throw new Error(`Refund failed: ${result.error}`)
103+
}
104+
105+
private async getOrCreateClient(customer: { name: string; email: string }) {
106+
return await Client.firstOrCreate({ email: customer.email }, { name: customer.name })
107+
}
108+
109+
private async getActiveGatewaysSortedByPriority() {
110+
return await Gateway.query().where('isActive', true).orderBy('priority', 'asc')
111+
}
112+
113+
private async createPendingTransaction(clientId: number, amount: number, cardNumber: string) {
114+
return await Transaction.create({
115+
clientId,
116+
amount,
117+
status: 'PENDING',
118+
cardLastNumbers: cardNumber.slice(-4),
119+
})
120+
}
121+
122+
private async attachProductsToTransaction(
123+
transaction: Transaction,
124+
products: { id: number; quantity: number }[]
125+
) {
126+
for (const p of products) {
127+
await transaction.related('products').attach({ [p.id]: { quantity: p.quantity } })
128+
}
129+
}
130+
131+
private buildPaymentRequest(
132+
client: Client,
133+
payment: { cardNumber: string; cvv: string },
134+
amount: number
135+
): PaymentRequest {
136+
return {
137+
amount,
138+
name: client.name,
139+
email: client.email,
140+
cardNumber: payment.cardNumber,
141+
cvv: payment.cvv,
142+
}
143+
}
144+
145+
private async finalizeTransaction(
146+
transaction: Transaction,
147+
gatewayId: number,
148+
externalId: string
149+
) {
150+
transaction.merge({
151+
status: 'PAID',
152+
gatewayId,
153+
externalId,
154+
})
155+
await transaction.save()
156+
}
157+
158+
private async markTransactionAsFailed(transaction: Transaction) {
159+
transaction.status = 'FAILED'
160+
await transaction.save()
99161
}
100162
}

0 commit comments

Comments
 (0)