changes
This commit is contained in:
@@ -135,9 +135,9 @@ $class-types: (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Numeric: w-0 → w-96, h-0 → h-96, size-0 → size-96 ───────────────────
|
// ─── Numeric: w-0 → w-400, h-0 → h-400, size-0 → size-400 ───────────────────
|
||||||
@each $prefix, $props in $class-types {
|
@each $prefix, $props in $class-types {
|
||||||
@for $i from 0 through 96 {
|
@for $i from 0 through 400 {
|
||||||
.#{$prefix}-#{$i} {
|
.#{$prefix}-#{$i} {
|
||||||
@include set-props($props, calc(var(--spacing) * #{$i}));
|
@include set-props($props, calc(var(--spacing) * #{$i}));
|
||||||
}
|
}
|
||||||
@@ -361,9 +361,9 @@ $inset-types: (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Numeric: 0 → 96, using the same --spacing scale ──────────────────────
|
// ─── Numeric: 0 → 400, using the same --spacing scale ──────────────────────
|
||||||
@each $prefix, $props in $inset-types {
|
@each $prefix, $props in $inset-types {
|
||||||
@for $i from 0 through 96 {
|
@for $i from 0 through 400 {
|
||||||
.#{$prefix}-#{$i} {
|
.#{$prefix}-#{$i} {
|
||||||
@include set-props($props, calc(var(--spacing) * #{$i}));
|
@include set-props($props, calc(var(--spacing) * #{$i}));
|
||||||
}
|
}
|
||||||
@@ -412,9 +412,9 @@ $minmax-types: (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ─── Numeric: 0 → 96, using --spacing scale ───────────────────────────────
|
// ─── Numeric: 0 → 400, using --spacing scale ───────────────────────────────
|
||||||
@each $prefix, $props in $minmax-types {
|
@each $prefix, $props in $minmax-types {
|
||||||
@for $i from 0 through 96 {
|
@for $i from 0 through 400 {
|
||||||
.#{$prefix}-#{$i} {
|
.#{$prefix}-#{$i} {
|
||||||
@include set-props($props, calc(var(--spacing) * #{$i}));
|
@include set-props($props, calc(var(--spacing) * #{$i}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import CartItem from "~/components/pos/CartItem.vue";
|
import CartItem from "~/components/pos/CartItem.vue";
|
||||||
|
import usePlaceOrder from "~/components/pos/composables/usePlaceOrder";
|
||||||
|
|
||||||
|
const id = "confirmOrderModal";
|
||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close"]);
|
||||||
const { $id, $patchapi, $deleteapi, $insertapi, $dayjs, $snackbar } = useNuxtApp();
|
|
||||||
const id = "confirmOrder";
|
|
||||||
const isPending = ref(false);
|
|
||||||
const { activeCart, activeCartItems, orderInfo, getCarts } = inject("pos");
|
const { activeCart, activeCartItems, orderInfo, getCarts } = inject("pos");
|
||||||
|
const { subtotal, shipping_address, isPending, showVietQRModal, placeOrder, testReturnPage } = usePlaceOrder({
|
||||||
const subtotal = computed(() => {
|
activeCart,
|
||||||
return activeCartItems.value?.reduce((prev, curr) => prev + curr.imei__variant__price, 0);
|
activeCartItems,
|
||||||
|
orderInfo,
|
||||||
|
getCarts,
|
||||||
|
onSuccess: () => emit("close"),
|
||||||
});
|
});
|
||||||
const shipping_address = computed(() => {
|
|
||||||
return `${orderInfo.value.address.address_detail}, ${orderInfo.value.address.ward}, ${orderInfo.value.address.district}, ${orderInfo.value.address.city}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function createOrder() {
|
|
||||||
try {
|
|
||||||
isPending.value = true;
|
|
||||||
|
|
||||||
const invoicePayload = {
|
|
||||||
customer: activeCart.value.customer,
|
|
||||||
customer_name: activeCart.value.customer__fullname,
|
|
||||||
customer_phone: activeCart.value.customer__phone,
|
|
||||||
customer_email: activeCart.value.customer__email,
|
|
||||||
shipping_address: shipping_address.value,
|
|
||||||
product_amount: activeCartItems.value.length,
|
|
||||||
shipping_fee: 0,
|
|
||||||
total_amount: subtotal.value,
|
|
||||||
discount_amount: 0,
|
|
||||||
final_amount: subtotal.value,
|
|
||||||
order_type: "pos",
|
|
||||||
status: "pending",
|
|
||||||
payment_status: 1,
|
|
||||||
staff: 1,
|
|
||||||
ordered_at: new Date(),
|
|
||||||
paid_at: null,
|
|
||||||
delivery_method: orderInfo.value.deliveryMethod.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const invoice = await $insertapi("Invoice", {
|
|
||||||
data: invoicePayload,
|
|
||||||
notify: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imeisSoldPayload = activeCartItems.value.map((cartItem) => ({
|
|
||||||
imei: cartItem.imei__imei,
|
|
||||||
variant: cartItem.imei__variant,
|
|
||||||
sold_date: $dayjs().format("YYYY-MM-DD"),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const imeisSold = await $insertapi("IMEI_Sold", {
|
|
||||||
data: imeisSoldPayload,
|
|
||||||
notify: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const invoiceDetailPayload = activeCartItems.value.map((cartItem) => {
|
|
||||||
const imeiSold = imeisSold.find((imeiSold) => imeiSold.imei === cartItem.imei__imei);
|
|
||||||
return {
|
|
||||||
invoice: invoice.id,
|
|
||||||
variant: cartItem.imei__variant,
|
|
||||||
imei_sold: imeiSold.id,
|
|
||||||
price: cartItem.imei__variant__price,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const invoiceDetails = await $insertapi("Invoice_Detail", {
|
|
||||||
data: invoiceDetailPayload,
|
|
||||||
notify: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (orderInfo.value.deliveryMethod.code === "HOME_DELIVERY") {
|
|
||||||
const deliveryInfoPayload = {
|
|
||||||
invoice: invoice.id,
|
|
||||||
receiver_name: orderInfo.value.receiver_name,
|
|
||||||
receiver_phone: orderInfo.value.receiver_phone,
|
|
||||||
city: orderInfo.value.address.city,
|
|
||||||
district: orderInfo.value.address.district,
|
|
||||||
ward: orderInfo.value.address.ward,
|
|
||||||
address: orderInfo.value.address.address_detail,
|
|
||||||
shipping_fee: 0,
|
|
||||||
carrier: orderInfo.value.deliveryMethod.id,
|
|
||||||
tracking_code: $id(),
|
|
||||||
status: "pending",
|
|
||||||
staff: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const deliveryInfo = await $insertapi("Delivery_Info", {
|
|
||||||
data: deliveryInfoPayload,
|
|
||||||
notify: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$snackbar("Tạo đơn hàng thành công", "Success");
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
$deleteapi(
|
|
||||||
"Cart_Item",
|
|
||||||
activeCartItems.value.map((c) => c.id),
|
|
||||||
),
|
|
||||||
$patchapi("Cart", {
|
|
||||||
id: activeCartItems.value[0].cart,
|
|
||||||
customer: null,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
getCarts();
|
|
||||||
emit("close");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
$snackbar("Tạo đơn hàng không thành công", "Error");
|
|
||||||
} finally {
|
|
||||||
isPending.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -231,7 +131,7 @@ async function createOrder() {
|
|||||||
Huỷ
|
Huỷ
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="createOrder"
|
@click="placeOrder"
|
||||||
:class="['button is-success', isPending && 'is-loading']"
|
:class="['button is-success', isPending && 'is-loading']"
|
||||||
>
|
>
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
@@ -242,8 +142,19 @@ async function createOrder() {
|
|||||||
</span>
|
</span>
|
||||||
<span>Đặt hàng</span>
|
<span>Đặt hàng</span>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- <button
|
||||||
|
@click="testReturnPage"
|
||||||
|
class="button is-yellow"
|
||||||
|
>
|
||||||
|
Test Return Page
|
||||||
|
</button> -->
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
<Modal
|
||||||
|
v-if="showVietQRModal"
|
||||||
|
v-bind="showVietQRModal"
|
||||||
|
@close="showVietQRModal = null"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
31
app/components/pos/VietQR.vue
Normal file
31
app/components/pos/VietQR.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
invoiceId: Number,
|
||||||
|
finalizeOrder: Function,
|
||||||
|
});
|
||||||
|
const { activeCartItems } = inject("pos");
|
||||||
|
const subtotal = computed(() => {
|
||||||
|
return activeCartItems.value?.reduce((prev, curr) => prev + curr.imei__variant__price, 0);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="is-flex is-flex-direction-column is-gap-2 is-align-items-center">
|
||||||
|
<NuxtImg
|
||||||
|
:src="qr(subtotal)"
|
||||||
|
class="h-full max-h-140"
|
||||||
|
alt="VietQR code"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="finalizeOrder(invoiceId)"
|
||||||
|
class="button is-primary"
|
||||||
|
>
|
||||||
|
<span class="icon">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:check-rounded"
|
||||||
|
:size="18"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>Xác nhận thanh toán</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -3,9 +3,10 @@ export default function useCustomer({ activeCart, getCarts }) {
|
|||||||
|
|
||||||
const addresses = ref([]);
|
const addresses = ref([]);
|
||||||
async function getAddresses() {
|
async function getAddresses() {
|
||||||
addresses.value = await $getdata("Customer_Address", {
|
addresses.value =
|
||||||
|
(await $getdata("Customer_Address", {
|
||||||
filter: { customer: activeCart.value.customer },
|
filter: { customer: activeCart.value.customer },
|
||||||
});
|
})) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const isChangingCus = ref(false);
|
const isChangingCus = ref(false);
|
||||||
|
|||||||
236
app/components/pos/composables/usePlaceOrder.js
Normal file
236
app/components/pos/composables/usePlaceOrder.js
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
export default function usePlaceOrder({ activeCart, activeCartItems, orderInfo, getCarts, onSuccess }) {
|
||||||
|
const { $dayjs, $id, $getdata, $insertapi, $patchapi, $deleteapi, $snackbar } = useNuxtApp();
|
||||||
|
const isPending = ref(false);
|
||||||
|
const paidPaymentStatus = ref();
|
||||||
|
const invoice = ref();
|
||||||
|
const showVietQRModal = ref();
|
||||||
|
|
||||||
|
const subtotal = computed(() => {
|
||||||
|
return activeCartItems.value?.reduce((prev, curr) => prev + curr.imei__variant__price, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipping_address = computed(() => {
|
||||||
|
if (!orderInfo.value.address) return "";
|
||||||
|
return `${orderInfo.value.address.address_detail}, ${orderInfo.value.address.ward}, ${orderInfo.value.address.district}, ${orderInfo.value.address.city}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a record in `invoice` as `pending`
|
||||||
|
*/
|
||||||
|
async function createOrder() {
|
||||||
|
try {
|
||||||
|
isPending.value = true;
|
||||||
|
|
||||||
|
const invoicePayload = {
|
||||||
|
customer: activeCart.value.customer,
|
||||||
|
customer_name: activeCart.value.customer__fullname,
|
||||||
|
customer_phone: activeCart.value.customer__phone,
|
||||||
|
customer_email: activeCart.value.customer__email,
|
||||||
|
shipping_address: shipping_address.value,
|
||||||
|
product_amount: activeCartItems.value.length,
|
||||||
|
shipping_fee: 0,
|
||||||
|
total_amount: subtotal.value,
|
||||||
|
discount_amount: 0,
|
||||||
|
final_amount: subtotal.value,
|
||||||
|
order_type: "pos",
|
||||||
|
status: "pending",
|
||||||
|
payment_status: 1,
|
||||||
|
staff: 1,
|
||||||
|
ordered_at: new Date(),
|
||||||
|
paid_at: null,
|
||||||
|
delivery_method: orderInfo.value.deliveryMethod.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoice = await $insertapi("Invoice", {
|
||||||
|
data: invoicePayload,
|
||||||
|
notify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return invoice;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
$snackbar("Tạo đơn hàng không thành công", "Error");
|
||||||
|
} finally {
|
||||||
|
isPending.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark sold IMEIs, create invoice details, delivery info, payment records, reset cart.
|
||||||
|
* Runs when order payment is confirmed.
|
||||||
|
* @param invoiceId id of created invoice
|
||||||
|
*/
|
||||||
|
async function finalizeOrder(invoiceId) {
|
||||||
|
if (!invoiceId) {
|
||||||
|
console.error("invoiceId is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imeisSold = await markImeisSold();
|
||||||
|
await createInvoiceDetails(invoiceId, imeisSold);
|
||||||
|
await createPaymentRecord(invoiceId);
|
||||||
|
|
||||||
|
if (orderInfo.value.deliveryMethod.code === "HOME_DELIVERY") {
|
||||||
|
await createDeliveryInfo(invoiceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update invoice.payment_status from PENDING to PAID
|
||||||
|
const patchedInvoice = await $patchapi("Invoice", { id: invoiceId, payment_status: paidPaymentStatus.value.id });
|
||||||
|
$snackbar("Khởi tạo đơn hàng thành công", "Success");
|
||||||
|
resetCart();
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markImeisSold() {
|
||||||
|
const imeisSoldPayload = activeCartItems.value.map((cartItem) => ({
|
||||||
|
imei: cartItem.imei__imei,
|
||||||
|
variant: cartItem.imei__variant,
|
||||||
|
sold_date: $dayjs().format("YYYY-MM-DD"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const imeisSold = await $insertapi("IMEI_Sold", {
|
||||||
|
data: imeisSoldPayload,
|
||||||
|
notify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return imeisSold;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createInvoiceDetails(invoiceId, imeisSold) {
|
||||||
|
const invoiceDetailPayload = activeCartItems.value.map((cartItem) => {
|
||||||
|
const imeiSold = imeisSold.find((imeiSold) => imeiSold.imei === cartItem.imei__imei);
|
||||||
|
return {
|
||||||
|
invoice: invoiceId,
|
||||||
|
variant: cartItem.imei__variant,
|
||||||
|
imei_sold: imeiSold.id,
|
||||||
|
price: cartItem.imei__variant__price,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const invoiceDetails = await $insertapi("Invoice_Detail", {
|
||||||
|
data: invoiceDetailPayload,
|
||||||
|
notify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return invoiceDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDeliveryInfo(invoiceId) {
|
||||||
|
const deliveryInfoPayload = {
|
||||||
|
invoice: invoiceId,
|
||||||
|
receiver_name: orderInfo.value.receiver_name,
|
||||||
|
receiver_phone: orderInfo.value.receiver_phone,
|
||||||
|
city: orderInfo.value.address.city,
|
||||||
|
district: orderInfo.value.address.district,
|
||||||
|
ward: orderInfo.value.address.ward,
|
||||||
|
address: orderInfo.value.address.address_detail,
|
||||||
|
shipping_fee: 0,
|
||||||
|
carrier: orderInfo.value.deliveryMethod.id,
|
||||||
|
tracking_code: $id(),
|
||||||
|
status: "pending",
|
||||||
|
staff: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const deliveryInfo = await $insertapi("Delivery_Info", {
|
||||||
|
data: deliveryInfoPayload,
|
||||||
|
notify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return deliveryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPaymentRecord(invoiceId) {
|
||||||
|
const paymentRecord = await $insertapi("Payment_Record", {
|
||||||
|
data: {
|
||||||
|
invoice: invoiceId,
|
||||||
|
payment_method: orderInfo.value.paymentMethod.id,
|
||||||
|
transfer_amount: orderInfo.value.paymentMethod.code === "CASH" ? undefined : subtotal.value,
|
||||||
|
cash_amount: orderInfo.value.paymentMethod.code === "CASH" ? subtotal.value : undefined,
|
||||||
|
},
|
||||||
|
notify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return paymentRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetCart() {
|
||||||
|
await Promise.all([
|
||||||
|
$deleteapi(
|
||||||
|
"Cart_Item",
|
||||||
|
activeCartItems.value.map((c) => c.id),
|
||||||
|
),
|
||||||
|
$patchapi("Cart", {
|
||||||
|
id: activeCartItems.value[0].cart,
|
||||||
|
customer: null,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
getCarts();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function placeOrder() {
|
||||||
|
invoice.value = await createOrder();
|
||||||
|
|
||||||
|
if (orderInfo.value.paymentMethod.code === "CASH") {
|
||||||
|
finalizeOrder(invoice.value.id);
|
||||||
|
} else if (orderInfo.value.paymentMethod.code === "VIETQR") {
|
||||||
|
showVietQRModal.value = {
|
||||||
|
component: "pos/VietQR",
|
||||||
|
title: "Thanh toán qua VietQR",
|
||||||
|
width: "700px",
|
||||||
|
vbind: { invoiceId: invoice.value.id, finalizeOrder }, // verify thủ công?
|
||||||
|
};
|
||||||
|
} else if (orderInfo.value.paymentMethod.code === "VNPAY") {
|
||||||
|
try {
|
||||||
|
const { paymentUrl } = await $fetch("/api/vnpay/create-payment", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
amount: invoice.value.total_amount,
|
||||||
|
invoiceCode: invoice.value.code,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(paymentUrl, "vnpaypopup", "popup=true,width=900,height=700");
|
||||||
|
listenToVnpayEvents();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function vnpayListener(e) {
|
||||||
|
console.log("main window received e.data from popup", e.data);
|
||||||
|
|
||||||
|
const { type, message } = e.data;
|
||||||
|
if (type === "vnpay-success") {
|
||||||
|
finalizeOrder(invoice.value.id);
|
||||||
|
}
|
||||||
|
if (type === "vnpay-error") {
|
||||||
|
$snackbar(`Thanh toán VNPAY thất bại: ${message}`, "Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenToVnpayEvents() {
|
||||||
|
window.removeEventListener("message", vnpayListener);
|
||||||
|
window.addEventListener("message", vnpayListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testReturnPage() {
|
||||||
|
listenToVnpayEvents();
|
||||||
|
|
||||||
|
const vnp_ResponseCode = "01";
|
||||||
|
window.open(
|
||||||
|
`http://localhost:3000/vnpay-return?vnp_Amount=1000000&vnp_BankCode=NCB&vnp_BankTranNo=VNP15593296&vnp_CardType=ATM&vnp_OrderInfo=Thanh+to%C3%A1n+%C4%91%C6%A1n+h%C3%A0ng+HD000001&vnp_PayDate=20260622142046&vnp_ResponseCode=${vnp_ResponseCode}&vnp_TmnCode=SCG4UUS6&vnp_TransactionNo=15593296&vnp_TransactionStatus=00&vnp_TxnRef=HD000001-2026-06-22T07:20:14.735Z&vnp_SecureHash=9726fe2db0da9741fc14fc42ced098cabf204f21ab12f633e8ac1cb7c9567a3b7a379d790f29a1a8b60fa8edb24acb0faca83feca0f7757865b0bf853b55ea01`,
|
||||||
|
"return",
|
||||||
|
"popup=true,width=800,height=600",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
paidPaymentStatus.value = await $getdata("Payment_Status", {
|
||||||
|
filter: { code: "PAID" },
|
||||||
|
first: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isPending, subtotal, shipping_address, placeOrder, testReturnPage, showVietQRModal };
|
||||||
|
}
|
||||||
50
app/pages/vnpay-return.vue
Normal file
50
app/pages/vnpay-return.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
function postMsgToMain(status: "success" | "error", message: string) {
|
||||||
|
window.opener.postMessage({ type: `vnpay-${status}`, message }, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const verifyResult = await $fetch("/api/vnpay/return", { query: route.query });
|
||||||
|
|
||||||
|
if (verifyResult.error) {
|
||||||
|
postMsgToMain("error", verifyResult.error);
|
||||||
|
} else {
|
||||||
|
postMsgToMain(verifyResult.isSuccess && verifyResult.isVerified ? "success" : "error", verifyResult.message);
|
||||||
|
}
|
||||||
|
// window.close();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="container my-20 is-flex is-flex-direction-column is-gap-2 is-align-items-center"
|
||||||
|
style="max-width: 1000px"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="svg-spinners:180-ring-with-bg"
|
||||||
|
:size="40"
|
||||||
|
class="has-text-primary"
|
||||||
|
/>
|
||||||
|
<p>Đang xác thực đơn hàng...</p>
|
||||||
|
<!-- debug -->
|
||||||
|
<!-- <div>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
@click="postMsgToMain('success', 'Success message')"
|
||||||
|
class="button is-light is-success"
|
||||||
|
>
|
||||||
|
post 'success'
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="postMsgToMain('error', 'Error message')"
|
||||||
|
class="button is-light is-danger"
|
||||||
|
>
|
||||||
|
post 'error'
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre>query: {{ route.query }}</pre>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
20
app/utils/qr.js
Normal file
20
app/utils/qr.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default function qr(amount, qrVars = {}) {
|
||||||
|
const defaultQRVars = {
|
||||||
|
bankId: "BIDV",
|
||||||
|
accountNo: "1222198820",
|
||||||
|
accountName: "Nguyen Viet An",
|
||||||
|
template: "Rxp4lcv",
|
||||||
|
description: "Thanh toan ERP",
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalVars = {
|
||||||
|
...defaultQRVars,
|
||||||
|
...qrVars,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { bankId, accountNo, template, description, accountName } = finalVars;
|
||||||
|
|
||||||
|
const qrSrc = `https://img.vietqr.io/image/${bankId}-${accountNo}-${template}.png?amount=${amount}&addInfo=${description}&accountName=${accountName}`;
|
||||||
|
|
||||||
|
return qrSrc;
|
||||||
|
}
|
||||||
35
server/api/vnpay/create-payment.post.ts
Normal file
35
server/api/vnpay/create-payment.post.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { VNPay } from "vnpay";
|
||||||
|
import { secureSecret, tmnCode, vnpayHost } from "~~/server/utils/vnpaySettings";
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody(event);
|
||||||
|
|
||||||
|
const vnpay = new VNPay({
|
||||||
|
// ⚡ Cấu hình bắt buộc
|
||||||
|
tmnCode,
|
||||||
|
secureSecret,
|
||||||
|
vnpayHost,
|
||||||
|
|
||||||
|
// 🔧 Cấu hình tùy chọn
|
||||||
|
testMode: true, // Chế độ test
|
||||||
|
// hashAlgorithm: 'SHA512', // Thuật toán mã hóa
|
||||||
|
enableLog: true, // Bật/tắt log
|
||||||
|
// loggerFn: ignoreLogger, // Custom logger
|
||||||
|
|
||||||
|
// 🔧 Custom endpoints
|
||||||
|
endpoints: {
|
||||||
|
paymentEndpoint: "paymentv2/vpcpay.html",
|
||||||
|
queryDrRefundEndpoint: "merchant_webapi/api/transaction",
|
||||||
|
getBankListEndpoint: "qrpayauth/api/merchant/get_bank_list",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentUrl = vnpay.buildPaymentUrl({
|
||||||
|
vnp_Amount: body.amount,
|
||||||
|
vnp_IpAddr: "192.168.1.1",
|
||||||
|
vnp_ReturnUrl: "http://localhost:3000/vnpay-return",
|
||||||
|
vnp_TxnRef: `Test-${body.invoiceCode}-${new Date().toISOString()}`,
|
||||||
|
vnp_OrderInfo: `Thanh toan don hang ${body.invoiceCode}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { paymentUrl };
|
||||||
|
});
|
||||||
36
server/api/vnpay/return.get.ts
Normal file
36
server/api/vnpay/return.get.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { VNPay } from "vnpay";
|
||||||
|
|
||||||
|
export default defineEventHandler<{
|
||||||
|
query: {
|
||||||
|
vnp_Amount: string;
|
||||||
|
vnp_BankCode: string;
|
||||||
|
vnp_BankTranNo: string;
|
||||||
|
vnp_CardType: string;
|
||||||
|
vnp_OrderInfo: string;
|
||||||
|
vnp_PayDate: string;
|
||||||
|
vnp_ResponseCode: string;
|
||||||
|
vnp_TmnCode: string;
|
||||||
|
vnp_TransactionNo: string;
|
||||||
|
vnp_TransactionStatus: string;
|
||||||
|
vnp_TxnRef: string;
|
||||||
|
vnp_SecureHash: string;
|
||||||
|
};
|
||||||
|
}>(async (event) => {
|
||||||
|
const query = getQuery(event);
|
||||||
|
const vnpay = new VNPay({ tmnCode, secureSecret, vnpayHost });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verify = vnpay.verifyReturnUrl(query);
|
||||||
|
/*
|
||||||
|
!isVerified -> Xác thực tính toàn vẹn dữ liệu thất bại
|
||||||
|
!isSuccess -> Đơn hàng thanh toán thất bại
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
isSuccess: verify.isSuccess,
|
||||||
|
isVerified: verify.isVerified,
|
||||||
|
message: verify.message,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { error: "Dữ liệu không hợp lệ" };
|
||||||
|
}
|
||||||
|
});
|
||||||
3
server/utils/vnpaySettings.ts
Normal file
3
server/utils/vnpaySettings.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const tmnCode = "SCG4UUS6";
|
||||||
|
export const secureSecret = "G0O1GV6Z5OS0YDG38O76QSYRR81D8WWY";
|
||||||
|
export const vnpayHost = "https://sandbox.vnpayment.vn/paymentv2/vpcpay.html";
|
||||||
Reference in New Issue
Block a user