Skip to content

Commit f6a3018

Browse files
committed
feat: add reference system
1 parent 9ebd436 commit f6a3018

File tree

15 files changed

+254
-52
lines changed

15 files changed

+254
-52
lines changed

app/pages/checkout.vue

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ useSeoMeta({
2121
2222
defineOgImageComponent('Saas')
2323
24+
const { t } = useI18n()
25+
2426
const route = useRoute()
2527
2628
const checkoutQr = computed(() => String(route.query.qr))
2729
const checkoutInfo = computed(() => {
28-
const query = parseQuery(checkoutQr.value)
30+
const query = parseQuery(checkoutQr.value.split('?')[1] || '')
2931
3032
return query
3133
})
@@ -49,11 +51,11 @@ whenever(data, (response) => {
4951
navigateTo({ path: '/app' })
5052
}
5153
else {
52-
notifyError({
53-
content: 'We have not received your payment yet. Please try again later, or contact support if the issue persists.',
54+
notifyWarning({
55+
content: t('We have not received your payment yet. Please try again later, or contact support if the issue persists.'),
5456
})
5557
}
56-
})
58+
}, { immediate: true })
5759
</script>
5860

5961
<template>
@@ -62,6 +64,12 @@ whenever(data, (response) => {
6264
:title="page.topup.title"
6365
:description="page.topup.description"
6466
>
67+
<UBanner
68+
icon="i-lucide-triangle-alert"
69+
color="warning"
70+
:title="$t('Modification of the transaction is prohibited and could potentially lead to account suspension!')"
71+
/>
72+
6573
<div class="grid md:grid-cols-2 gap-4 items-start">
6674
<div class="flex justify-center">
6775
<img
@@ -84,9 +92,14 @@ whenever(data, (response) => {
8492
<span class="text-lg font-semibold text-gray-900 dark:text-white">{{ checkoutInfo.bank }}</span>
8593
</div>
8694

95+
<div class="flex justify-between items-center">
96+
<span class="text-lg font-medium text-gray-500 dark:text-gray-400">{{ $t('Bank Number') }}</span>
97+
<span class="text-lg font-semibold text-gray-900 dark:text-white">{{ checkoutInfo.acc }}</span>
98+
</div>
99+
87100
<div class="flex justify-between items-center">
88101
<span class="text-lg font-medium text-gray-500 dark:text-gray-400">{{ $t('Amount') }}</span>
89-
<span class="text-xl font-bold text-primary-500 dark:text-primary-400">{{ checkoutInfo.amount }}</span>
102+
<span class="text-xl font-bold text-primary-500 dark:text-primary-400">{{ new Intl.NumberFormat('en-US', { style: 'currency', currency: 'VND' }).format(Number(checkoutInfo.amount)) }}</span>
90103
</div>
91104

92105
<div class="flex justify-between items-start">

i18n/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,13 @@
228228
"Infor": "Infor",
229229
"Payment Summary": "Payment Summary",
230230
"Bank Name": "Bank Name",
231+
"Bank Number": "Bank Number",
231232
"Amount": "Amount",
232233
"Description": "Description",
234+
"Discount": "Discount",
235+
"We have not received your payment yet. Please try again later, or contact support if the issue persists.": "We have not received your payment yet. Please try again later, or contact support if the issue persists.",
233236
"Please verify the details before proceeding with the payment.": "Please verify the details before proceeding with the payment.",
234-
"I have transfered the money! (Click here)": "I have transfered the money! (Click here)"
237+
"Modification of the transaction is prohibited and could potentially lead to account suspension!": "Modification of the transaction is prohibited and could potentially lead to account suspension!",
238+
"I have transfered the money! (Click here)": "I have transfered the money! (Click here)",
239+
"No discount applied!": "No discount applied!"
235240
}

i18n/vi.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,13 @@
228228
"Infor": "Thông tin",
229229
"Payment Summary": "Thông tin thanh toán",
230230
"Bank Name": "Ngân hàng",
231+
"Bank Number": "Số tài khoản",
231232
"Amount": "Số tiền",
232233
"Description": "Mô tả",
233-
"Please verify the details before proceeding with the payment.": "Vui lòng xác minh thông tin trước khi tiếp tục thanh toán.",
234-
"I have transfered the money! (Click here)": "Tôi đã chuyển tiền! (Nhấp vào đây)"
234+
"Discount": "Giảm giá",
235+
"We have not received your payment yet. Please try again later, or contact support if the issue persists.": "Chúng tôi chưa nhận được thanh toán của bạn. Vui lòng thử lại trong giây lát, hoặc liên hệ với bộ phận hỗ trợ nếu vấn đề vẫn tiếp diễn.",
236+
"Please verify the details before proceeding with the payment.": "Vui lòng xác minh các chi tiết trước khi tiếp tục thanh toán.",
237+
"Modification of the transaction is prohibited and could potentially lead to account suspension!": "Việc sửa đổi nội dung giao dịch bị cấm và có thể dẫn đến việc tạm ngưng tài khoản!",
238+
"I have transfered the money! (Click here)": "Tôi đã chuyển tiền! (Nhấp vào đây)",
239+
"No discount applied!": "Không có mã giảm giá!"
235240
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@thecodeorigin/nuxt",
33
"type": "module",
4-
"version": "2.6.3",
4+
"version": "2.7.0",
55
"publishConfig": {
66
"registry": "https://registry.npmjs.org",
77
"access": "public"

server/api/payments/sepay/webhook.post.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,21 @@ import { z } from 'zod'
33

44
export default defineEventHandler(async (event) => {
55
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-
// }
206
const body = await readValidatedBody(
217
event,
228
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(),
9+
accountNumber: z.string(), // e.g., "17228427"
10+
accumulated: z.number(), // e.g., 0
11+
code: z.string(), // e.g., "SPN8NHOSTING123"
12+
content: z.string(), // e.g., "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+
description: z.string(), // e.g., "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"
14+
gateway: z.string(), // e.g., "ACB"
15+
id: z.number(), // e.g., 13425123
16+
referenceCode: z.string(), // e.g., "3165"
17+
subAccount: z.string().optional().nullable(), // e.g., null
18+
transactionDate: z.string(), // e.g., "2025-05-23 23:34:40"
19+
transferAmount: z.number(), // e.g., 2000
20+
transferType: z.string(), // e.g., "in"
3521
}).parse(payload),
3622
)
3723

@@ -61,10 +47,20 @@ export default defineEventHandler(async (event) => {
6147

6248
logger.log(`[SePay Webhook] Processing transaction: code=${orderCode}, status=${transactionStatus}`)
6349

64-
const priceDiscount = Number(paymentTransactionOfProvider.payment.order.package.price_discount)
65-
const price = Number(paymentTransactionOfProvider.payment.order.package.price)
50+
const userId = paymentTransactionOfProvider.payment.order.user_id
51+
52+
const { getUserBestPrice, createReferenceUsage } = useReference()
53+
54+
const reference = paymentTransactionOfProvider.payment.order.reference
55+
56+
const price = await getUserBestPrice(
57+
userId,
58+
Number(paymentTransactionOfProvider.payment.order.package.price),
59+
Number(paymentTransactionOfProvider.payment.order.package.price_discount),
60+
reference?.code,
61+
)
6662

67-
if (priceDiscount !== Number(body.transferAmount) && price !== Number(body.transferAmount)) {
63+
if (price !== Number(body.transferAmount)) {
6864
logger.error(`[SePay Webhook] Amount mismatch, transaction [${paymentTransactionOfProvider.id}]: expected=${price}, received=${body.transferAmount}`)
6965

7066
throw createError({
@@ -74,7 +70,6 @@ export default defineEventHandler(async (event) => {
7470
}
7571

7672
const creditAmount = Number(paymentTransactionOfProvider.payment.order.package.amount)
77-
const userId = paymentTransactionOfProvider.payment.order.user_id
7873

7974
// The userId is already the UUID from our database since we've updated
8075
// our schemas to use UUID references between tables
@@ -98,6 +93,14 @@ export default defineEventHandler(async (event) => {
9893

9994
await updatePaymentStatus(paymentTransactionOfProvider.payment.id, transactionStatus)
10095

96+
if (reference) {
97+
await createReferenceUsage(
98+
userId,
99+
reference.id || '',
100+
paymentTransactionOfProvider.id,
101+
)
102+
}
103+
101104
logger.log(`[SePay Webhook] Transaction updated successfully: id=${paymentTransactionOfProvider.id}, status=${transactionStatus}`)
102105

103106
logger.log('[SePay Webhook] Webhook processing completed successfully')

server/api/ref/[referCode].get.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { withQuery } from 'ufo'
2+
3+
export default defineEventHandler(async (event) => {
4+
try {
5+
const { referCode } = await defineEventOptions(event, { params: [REFERENCE_CODE_COOKIE_NAME] })
6+
7+
setCookie(event, REFERENCE_CODE_COOKIE_NAME, referCode, {
8+
httpOnly: true,
9+
})
10+
11+
return sendRedirect(event, withQuery('/pricing', { referCode }), 301)
12+
}
13+
catch (error: any) {
14+
throw createError({
15+
statusCode: 503,
16+
statusMessage: error.message,
17+
})
18+
}
19+
})

server/composables/usePayment.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { PaymentStatus, orderTable, paymentProviderTransactionTable, paymentTabl
33
import type { Order, Payment, PaymentProviderTransaction } from '../types/models'
44

55
export function usePayment() {
6-
async function createOrder(productId: string, userId: string): Promise<Order> {
6+
async function createOrder(productId: string, userId: string, referenceId?: string): Promise<Order> {
77
return (
88
await db.insert(orderTable).values({
99
product_id: productId,
1010
user_id: userId,
11+
reference_id: referenceId,
1112
}).returning()
1213
)[0]
1314
}
@@ -55,6 +56,7 @@ export function usePayment() {
5556
| (PaymentProviderTransaction & {
5657
payment: Payment & {
5758
order: Order & {
59+
reference: any
5860
package: any
5961
}
6062
}
@@ -68,6 +70,7 @@ export function usePayment() {
6870
with: {
6971
order: {
7072
with: {
73+
reference: true,
7174
package: true,
7275
},
7376
},

server/composables/useReference.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { eq, sql } from 'drizzle-orm'
2+
import { referenceTable, referenceUsageTable } from '../db/schemas'
3+
4+
export const REFERENCE_CODE_COOKIE_NAME = 'referCode'
5+
6+
export function useReference() {
7+
function getReferenceById(referenceId: string) {
8+
return db.query.referenceTable.findFirst({
9+
where(schema, { eq }) {
10+
return eq(schema.id, referenceId)
11+
},
12+
})
13+
}
14+
15+
function getReferenceByCode(referenceCode: string) {
16+
return db.query.referenceTable.findFirst({
17+
where(schema, { eq }) {
18+
return eq(schema.code, referenceCode)
19+
},
20+
})
21+
}
22+
23+
function getUserReferenceUsage(userId: string) {
24+
return db.query.referenceUsageTable.findFirst({
25+
where(schema, { eq }) {
26+
return eq(schema.user_id, userId)
27+
},
28+
})
29+
}
30+
31+
async function getUserBestPrice(userId: string, originalPrice: number, discountPrice?: number | null, referCode?: string | null) {
32+
const userReferenceUsage = await getUserReferenceUsage(userId)
33+
34+
let price = originalPrice
35+
36+
if (!userReferenceUsage && referCode) {
37+
const reference = await getReferenceByCode(referCode)
38+
39+
if (reference) {
40+
const referenceInStock = reference.quantity === null || reference.quantity > 0
41+
42+
if (referenceInStock && reference?.percentage) {
43+
price = originalPrice * (1 - reference.percentage / 100)
44+
}
45+
else if (referenceInStock && reference?.amount) {
46+
price = originalPrice - reference.amount
47+
}
48+
}
49+
}
50+
51+
// use the best price for the customer
52+
price = Math.ceil(
53+
discountPrice
54+
? Math.min(discountPrice, price)
55+
: price,
56+
)
57+
58+
if (!price) {
59+
throw createError({
60+
statusCode: 400,
61+
statusMessage: ErrorMessage.BAD_REQUEST,
62+
})
63+
}
64+
65+
return price
66+
}
67+
68+
async function createReferenceUsage(userId: string, referenceId: string, paymentProviderTransactionId: string) {
69+
const referenceUsage = await db.insert(referenceUsageTable).values({
70+
user_id: userId,
71+
reference_id: referenceId,
72+
payment_provider_transaction_id: paymentProviderTransactionId,
73+
}).returning()
74+
75+
await db.update(referenceTable)
76+
.set({
77+
quantity: sql`${referenceTable.quantity} - 1`,
78+
})
79+
.where(eq(referenceTable.id, referenceId))
80+
81+
return referenceUsage[0]
82+
}
83+
84+
return {
85+
getReferenceById,
86+
getUserReferenceUsage,
87+
getUserBestPrice,
88+
createReferenceUsage,
89+
}
90+
}

server/db/schemas/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export * from './products.schema'
44

55
export * from './enum.schema'
66

7+
export * from './identities.schema'
8+
79
export * from './payment_provider_transactions.schema'
810

911
export * from './notifications.schema'
@@ -16,4 +18,6 @@ export * from './payments.schema'
1618

1719
export * from './users.schema'
1820

19-
export * from './identities.schema'
21+
export * from './reference_usages.schema'
22+
23+
export * from './references.schema'

server/db/schemas/orders.schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import { relations } from 'drizzle-orm/relations'
33
import { paymentTable } from './payments.schema'
44
import { productTable } from './products.schema'
55
import { userTable } from './users.schema'
6+
import { referenceTable } from './references.schema'
67

78
export const orderTable = pgTable('orders', {
89
id: uuid('id').defaultRandom().primaryKey().notNull(),
910
user_id: uuid('user_id')
1011
.references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }).notNull(),
1112
product_id: uuid('product_id')
1213
.references(() => productTable.id, { onDelete: 'no action', onUpdate: 'no action' }),
14+
reference_id: uuid('reference_id')
15+
.references(() => referenceTable.id, { onDelete: 'no action', onUpdate: 'no action' }),
1316
created_at: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
1417
updated_at: timestamp('updated_at', { withTimezone: true }).defaultNow().$onUpdate(() => new Date()),
1518
})
@@ -27,4 +30,8 @@ export const userOrderRelations = relations(orderTable, ({ one }) => ({
2730
fields: [orderTable.user_id],
2831
references: [userTable.id],
2932
}),
33+
reference: one(referenceTable, {
34+
fields: [orderTable.reference_id],
35+
references: [referenceTable.id],
36+
}),
3037
}))

0 commit comments

Comments
 (0)