Skip to content

Commit 596890d

Browse files
committed
feat: wip sepay
1 parent e11e859 commit 596890d

File tree

5 files changed

+164
-2
lines changed

5 files changed

+164
-2
lines changed

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ export default defineNuxtConfig({
285285
'/blog/**': { swr: true },
286286
'/api/content/**': { csurf: false },
287287
'/api/logto/webhook': { csurf: false },
288+
'/api/payments/sepay/webhook': { csurf: false },
288289
'/api/payments/payos/webhook': { csurf: false },
289290
'/api/payments/vnpay/callback': { csurf: false },
290291
'/api/payments/vnpay/IPN': { csurf: false },
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export default defineEventHandler(async (event) => {
2+
try {
3+
const { session } = await defineEventOptions(event, { auth: true })
4+
const { productIdentifier } = await readBody(event)
5+
6+
const paymentUrl = await createPaymentCheckout('sepay', {
7+
productIdentifier,
8+
user: session,
9+
})
10+
11+
return {
12+
data: {
13+
paymentUrl,
14+
},
15+
}
16+
}
17+
catch (error: any) {
18+
logger.error('[Payment API] Error creating SePay checkout URL:', error)
19+
20+
throw parseError(error)
21+
}
22+
})
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { PaymentStatus } from '@base/server/db/schemas'
2+
import { z } from 'zod'
3+
4+
export default defineEventHandler(async (event) => {
5+
try {
6+
// {
7+
// gateway: "ACB",
8+
// transactionDate: "2025-05-23 23:34:40",
9+
// accountNumber: "17228427",
10+
// subAccount: null,
11+
// code: "SPN8NHOSTING123",
12+
// content: "MBVCB.9604208212.518683.SPN8NHOSTING123.CT tu 0041000331568 NGUYEN HUU NGUYEN Y toi 17228427 NGUYEN HUU NGUYEN Y tai ACB GD 518683-052325 23:34:40",
13+
// transferType: "in",
14+
// description: "BankAPINotify MBVCB.9604208212.518683.SPN8NHOSTING123.CT tu 0041000331568 NGUYEN HUU NGUYEN Y toi 17228427 NGUYEN HUU NGUYEN Y tai ACB GD 518683-052325 23:34:40",
15+
// transferAmount: 2000,
16+
// referenceCode: "3165",
17+
// accumulated: 0,
18+
// id: 13425123,
19+
// }
20+
const body = await readValidatedBody(
21+
event,
22+
payload => z.object({
23+
accountNumber: z.string(),
24+
accumulated: z.number(),
25+
code: z.string(),
26+
content: z.string(),
27+
description: z.string(),
28+
gateway: z.string(),
29+
id: z.number(),
30+
referenceCode: z.string(),
31+
subAccount: z.string().optional().nullable(),
32+
transactionDate: z.string(),
33+
transferAmount: z.number(),
34+
transferType: z.string(),
35+
}).partial().parse(payload),
36+
)
37+
38+
if (process.env.SEPAY_WEBHOOK_SIGNING_KEY !== getHeader(event, 'Authorization')?.match(/Apikey (.*)/)?.[1]) {
39+
logger.error('[SePay Webhook] Invalid webhook authentication')
40+
throw createError({
41+
statusCode: 401,
42+
message: 'Invalid webhook authentication',
43+
})
44+
}
45+
46+
logger.log('[SePay Webhook] Verified webhook data:', body)
47+
48+
// SePay Webhook always success (if not, it will not call this endpoint anyway)
49+
50+
const transactionStatus = PaymentStatus.RESOLVED
51+
52+
const { updatePaymentStatus, updateProviderTransactionStatus, getProviderTransactionByOrderCode } = usePayment()
53+
54+
const paymentTransactionOfProvider = await getProviderTransactionByOrderCode(String(body.code))
55+
56+
if (!paymentTransactionOfProvider?.payment.order.package) {
57+
logger.warn(`[SePay Webhook] Transaction not found or invalid: code=${body.code}`)
58+
return { success: true }
59+
}
60+
61+
logger.log(`[SePay Webhook] Processing transaction: code=${body.code}, status=${transactionStatus}`)
62+
63+
const priceDiscount = Number(paymentTransactionOfProvider.payment.order.package.price_discount)
64+
const price = Number(paymentTransactionOfProvider.payment.order.package.price)
65+
66+
if (priceDiscount !== Number(body.transferAmount) && price !== Number(body.transferAmount)) {
67+
logger.error(`[SePay Webhook] Amount mismatch, transaction [${paymentTransactionOfProvider.id}]: expected=${price}, received=${body.transferAmount}`)
68+
69+
throw createError({
70+
statusCode: 400,
71+
message: 'Amount mismatch!',
72+
})
73+
}
74+
75+
const creditAmount = Number(paymentTransactionOfProvider.payment.order.package.amount)
76+
const userId = paymentTransactionOfProvider.payment.order.user_id
77+
78+
// The userId is already the UUID from our database since we've updated
79+
// our schemas to use UUID references between tables
80+
logger.log(`[SePay Webhook] Adding credits: userId=${userId}, amount=${creditAmount}`)
81+
82+
await addCreditToUser(userId, creditAmount)
83+
84+
logger.log(`[SePay Webhook] Credits added successfully: userId=${userId}, amount=${creditAmount}`)
85+
86+
if (!paymentTransactionOfProvider?.payment.order.package) {
87+
logger.error(`[SePay Webhook] No product found for transaction: ${body.code}`)
88+
throw createError({
89+
statusCode: 400,
90+
message: 'No product found for this transaction!',
91+
})
92+
}
93+
94+
logger.log(`[SePay Webhook] Updating transaction ${paymentTransactionOfProvider.id} to status: ${transactionStatus}`)
95+
96+
await updateProviderTransactionStatus(paymentTransactionOfProvider.id, transactionStatus, body.transactionDate!)
97+
98+
await updatePaymentStatus(paymentTransactionOfProvider.payment.id, transactionStatus)
99+
100+
logger.log(`[SePay Webhook] Transaction updated successfully: id=${paymentTransactionOfProvider.id}, status=${transactionStatus}`)
101+
102+
logger.log('[SePay Webhook] Webhook processing completed successfully')
103+
return { success: true }
104+
}
105+
catch (error: any) {
106+
logger.error('[SePay Webhook] Error processing webhook:', error)
107+
108+
throw parseError(error)
109+
}
110+
})

server/utils/payment/vn/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createPayOSCheckout } from './payos'
44
export * from './payos'
55

66
export async function createPaymentCheckout(
7-
provider: 'payos' | 'vnpay',
7+
provider: 'payos' | 'vnpay' | 'sepay',
88
payload: {
99
clientIP?: string
1010
productIdentifier: string
@@ -67,11 +67,17 @@ export async function createPaymentCheckout(
6767
case 'payos':
6868
return await createPayOSCheckout({
6969
orderCode,
70-
amount: Number(userPayment.amount),
70+
amount: userPayment.amount,
7171
buyerEmail: payload.user.primary_email as string,
7272
buyerPhone: payload.user.primary_phone as string,
7373
paymentProviderTransaction,
7474
})
75+
case 'sepay':
76+
return await createSePayCheckout({
77+
orderCode,
78+
amount: userPayment.amount,
79+
paymentProviderTransaction,
80+
})
7581

7682
default:
7783
throw createError({

server/utils/payment/vn/sepay.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { paymentProviderTransactionTable } from '@base/server/db/schemas'
2+
import { withQuery } from 'ufo'
3+
4+
interface SePayCheckoutProps {
5+
orderCode: string
6+
amount: number
7+
paymentProviderTransaction: typeof paymentProviderTransactionTable.$inferSelect
8+
}
9+
10+
export async function createSePayCheckout({
11+
orderCode,
12+
amount,
13+
paymentProviderTransaction,
14+
}: SePayCheckoutProps) {
15+
return withQuery('https://qr.sepay.vn/img', {
16+
acc: '17228427',
17+
bank: 'ACB',
18+
amount,
19+
des: [orderCode, paymentProviderTransaction.provider_transaction_info].join('.'),
20+
template: 'compact',
21+
download: false,
22+
})
23+
}

0 commit comments

Comments
 (0)