payme price calculation update on modal
This commit is contained in:
@@ -1,16 +1,5 @@
|
|||||||
// ─── Domain Types ──────────────────────────────────────────────────────────────
|
// ─── Domain Types ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface ServicePricing {
|
|
||||||
serviceFee: number;
|
|
||||||
certificateFee: number;
|
|
||||||
currency: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrderSummary {
|
|
||||||
hasCertificate: boolean;
|
|
||||||
pricing: ServicePricing;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymePaymentRequest {
|
export interface PaymePaymentRequest {
|
||||||
amount: number; // in tiyin (1 UZS = 100 tiyin)
|
amount: number; // in tiyin (1 UZS = 100 tiyin)
|
||||||
orderId: string;
|
orderId: string;
|
||||||
@@ -26,20 +15,21 @@ export interface PaymePaymentResponse {
|
|||||||
export type PaymentStatus = 'idle' | 'loading' | 'success' | 'error';
|
export type PaymentStatus = 'idle' | 'loading' | 'success' | 'error';
|
||||||
|
|
||||||
// ─── Component Props ───────────────────────────────────────────────────────────
|
// ─── Component Props ───────────────────────────────────────────────────────────
|
||||||
|
export interface PriceCalculate {
|
||||||
|
service_fee: number;
|
||||||
|
discount?: number;
|
||||||
|
total_price: number;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaymentModalProps {
|
export interface PaymentModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
hasCertificate: boolean;
|
price: PriceCalculate;
|
||||||
onConfirmPayment: () => void;
|
onConfirmPayment: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PriceSummaryProps {
|
|
||||||
hasCertificate: boolean;
|
|
||||||
pricing: ServicePricing;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymeButtonProps {
|
export interface PaymeButtonProps {
|
||||||
amount: number;
|
amount: number;
|
||||||
orderId: string;
|
orderId: string;
|
||||||
20
src/features/modals/paymentModal/lib/utils.ts
Normal file
20
src/features/modals/paymentModal/lib/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// ─── Pricing Utilities ─────────────────────────────────────────────────────────
|
||||||
|
export const formatPrice = (amount: number, currency: string): string =>
|
||||||
|
`${amount.toLocaleString('uz-UZ')} ${currency}`;
|
||||||
|
|
||||||
|
// ─── Order ID Generator ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const generateOrderId = (): string => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).slice(2, 8).toUpperCase();
|
||||||
|
return `ORDER-${timestamp}-${random}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Payme API ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects the user to the Payme checkout page.
|
||||||
|
*/
|
||||||
|
export const redirectToPayme = (redirectUrl: string): void => {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { PaymentModalProps } from '../lib/types';
|
import { PaymentModalProps } from '../lib/types';
|
||||||
import { getPricing } from '../lib/utils';
|
|
||||||
import { PriceSummary } from './Pricesummary';
|
import { PriceSummary } from './Pricesummary';
|
||||||
import { PaymeButton } from './Paymebutton';
|
import { PaymeButton } from './Paymebutton';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@@ -85,12 +84,11 @@ const SecurityBadge: React.FC<{ securityText: string }> = ({
|
|||||||
export const PaymentModal: React.FC<PaymentModalProps> = ({
|
export const PaymentModal: React.FC<PaymentModalProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
hasCertificate,
|
price,
|
||||||
onConfirmPayment,
|
onConfirmPayment,
|
||||||
isLoading,
|
isLoading,
|
||||||
}) => {
|
}) => {
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
const pricing = getPricing();
|
|
||||||
const status = isLoading ? 'loading' : 'idle';
|
const status = isLoading ? 'loading' : 'idle';
|
||||||
const t = useTranslations('Payment');
|
const t = useTranslations('Payment');
|
||||||
|
|
||||||
@@ -174,11 +172,10 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
|||||||
<h3 className="text-xs font-semibold uppercase tracking-widest text-slate-400 mb-3">
|
<h3 className="text-xs font-semibold uppercase tracking-widest text-slate-400 mb-3">
|
||||||
{t('orderSummary')}
|
{t('orderSummary')}
|
||||||
</h3>
|
</h3>
|
||||||
<PriceSummary hasCertificate={hasCertificate} pricing={pricing} />
|
<PriceSummary priceCalculate={price} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Certificate badge */}
|
{/* Certificate badge */}
|
||||||
{hasCertificate && (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-emerald-700 bg-emerald-50 border border-emerald-100 rounded-lg px-3.5 py-2.5">
|
<div className="flex items-center gap-2 text-sm text-emerald-700 bg-emerald-50 border border-emerald-100 rounded-lg px-3.5 py-2.5">
|
||||||
<svg
|
<svg
|
||||||
width="15"
|
width="15"
|
||||||
@@ -191,7 +188,6 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
|
|||||||
</svg>
|
</svg>
|
||||||
<span>{t('certificateIncluded')}</span>
|
<span>{t('certificateIncluded')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Payment method label */}
|
{/* Payment method label */}
|
||||||
<div>
|
<div>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { formatPrice } from '../lib/utils';
|
import { formatPrice } from '../lib/utils';
|
||||||
import { PriceSummaryProps } from '../lib/types';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { PriceCalculate } from '../lib/types';
|
||||||
|
|
||||||
// ─── Price Row ─────────────────────────────────────────────────────────────────
|
// ─── Price Row ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -47,34 +47,33 @@ const PriceRow: React.FC<PriceRowProps> = ({
|
|||||||
|
|
||||||
// ─── Price Summary ─────────────────────────────────────────────────────────────
|
// ─── Price Summary ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const PriceSummary: React.FC<PriceSummaryProps> = ({
|
export const PriceSummary = ({
|
||||||
hasCertificate,
|
priceCalculate,
|
||||||
pricing,
|
}: {
|
||||||
|
priceCalculate: PriceCalculate;
|
||||||
}) => {
|
}) => {
|
||||||
console.log(hasCertificate);
|
|
||||||
const total = 41200;
|
|
||||||
const t = useTranslations('Payment');
|
const t = useTranslations('Payment');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-slate-50 border border-slate-100 px-5 py-2 space-y-0">
|
<div className="rounded-xl bg-slate-50 border border-slate-100 px-5 py-2 space-y-0">
|
||||||
<PriceRow
|
<PriceRow
|
||||||
label={t('serviceFee')}
|
label={t('serviceFee')}
|
||||||
amount={41200}
|
amount={priceCalculate.service_fee || 0}
|
||||||
currency={pricing.currency}
|
currency={priceCalculate.currency}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* {hasCertificate && (
|
{priceCalculate.discount && (
|
||||||
<PriceRow
|
<PriceRow
|
||||||
label={t('certificateLabel')}
|
label={t('certificateLabel')}
|
||||||
amount={pricing.certificateFee}
|
amount={priceCalculate.discount}
|
||||||
currency={pricing.currency}
|
currency={priceCalculate.currency}
|
||||||
/>
|
/>
|
||||||
)} */}
|
)}
|
||||||
|
|
||||||
<PriceRow
|
<PriceRow
|
||||||
label={t('total')}
|
label={t('total')}
|
||||||
amount={total}
|
amount={priceCalculate.total_price}
|
||||||
currency={pricing.currency}
|
currency={priceCalculate.currency}
|
||||||
highlight
|
highlight
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -7,7 +7,7 @@ import { useParams } from 'next/navigation';
|
|||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
import PaymentStatus from './paidStatus';
|
import PaymentStatus from './paidStatus';
|
||||||
import Sertifikat from './ui/sertificate/sertifikat';
|
import Sertifikat from '@/features/modals/sertificateModal/sertifikat';
|
||||||
|
|
||||||
// ── Types ────────────────────────────────────────────────────────────────────
|
// ── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
StatusBanner,
|
StatusBanner,
|
||||||
} from './Plagiraismui';
|
} from './Plagiraismui';
|
||||||
import { usePlagiarismForm } from '../lib/usePlagiraism';
|
import { usePlagiarismForm } from '../lib/usePlagiraism';
|
||||||
import { PaymentModal } from '@/widgets/paymentModal/ui/Paymentmodal';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { DOCUMENT_TYPES } from '@/widgets/detail/ui/sertificate/types';
|
import { DOCUMENT_TYPES } from '@/features/modals/sertificateModal/types';
|
||||||
|
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
||||||
|
|
||||||
const inputCls = `
|
const inputCls = `
|
||||||
w-full px-3.5 py-3.5 text-[14px] text-slate-800
|
w-full px-3.5 py-3.5 text-[14px] text-slate-800
|
||||||
@@ -218,7 +218,12 @@ export function PlagiarismCheckForm() {
|
|||||||
<PaymentModal
|
<PaymentModal
|
||||||
isOpen={isPaymentOpen}
|
isOpen={isPaymentOpen}
|
||||||
onClose={() => setIsPaymentOpen(false)}
|
onClose={() => setIsPaymentOpen(false)}
|
||||||
hasCertificate={form.certificate}
|
price={{
|
||||||
|
service_fee: 41200,
|
||||||
|
discount: 5200,
|
||||||
|
total_price: 36000,
|
||||||
|
currency: 'UZS',
|
||||||
|
}}
|
||||||
onConfirmPayment={handleSubmit}
|
onConfirmPayment={handleSubmit}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// ─── Domain Types ──────────────────────────────────────────────────────────────
|
// ─── Domain Types ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { PriceCalculate } from '@/features/modals/paymentModal/lib/types';
|
||||||
|
|
||||||
export type CheckResult = 'clean' | 'plagiarism_found' | 'pending' | 'failed';
|
export type CheckResult = 'clean' | 'plagiarism_found' | 'pending' | 'failed';
|
||||||
|
|
||||||
export interface DocumentData {
|
export interface DocumentData {
|
||||||
@@ -13,6 +15,7 @@ export interface DocumentData {
|
|||||||
results: [];
|
results: [];
|
||||||
state: 'paid' | 'unpaid';
|
state: 'paid' | 'unpaid';
|
||||||
order_id: number;
|
order_id: number;
|
||||||
|
price_calculation?: PriceCalculate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlagiarismCheckDetail extends DocumentData {
|
export interface PlagiarismCheckDetail extends DocumentData {
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { formatDate } from '../lib/utils';
|
|||||||
import { useRouter } from '@/shared/config/i18n/navigation';
|
import { useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
import { useUserPlagiatStore } from '@/shared/zustand/user';
|
||||||
import PaymentStatus from '@/widgets/detail/paidStatus';
|
import PaymentStatus from '@/widgets/detail/paidStatus';
|
||||||
import { PaymentModal } from '@/widgets/paymentModal/ui/Paymentmodal';
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { apiRequest } from '@/shared/request/apiRequest';
|
import { apiRequest } from '@/shared/request/apiRequest';
|
||||||
import { links } from '@/shared/request/links';
|
import { links } from '@/shared/request/links';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
|
||||||
|
|
||||||
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -160,7 +160,12 @@ export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
|
|||||||
<PaymentModal
|
<PaymentModal
|
||||||
isOpen={isPaymentOpen}
|
isOpen={isPaymentOpen}
|
||||||
onClose={() => setIsPaymentOpen(false)}
|
onClose={() => setIsPaymentOpen(false)}
|
||||||
hasCertificate={false}
|
price={{
|
||||||
|
service_fee: 41200,
|
||||||
|
discount: 5200,
|
||||||
|
total_price: 36000,
|
||||||
|
currency: 'UZS',
|
||||||
|
}}
|
||||||
onConfirmPayment={() => {
|
onConfirmPayment={() => {
|
||||||
handleSubmit({ document_id: Number(item.order_id) });
|
handleSubmit({ document_id: Number(item.order_id) });
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
// ─── Pricing Utilities ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
import { PAYME_CONFIG, PRICING } from './constant';
|
|
||||||
import {
|
|
||||||
PaymePaymentRequest,
|
|
||||||
PaymePaymentResponse,
|
|
||||||
ServicePricing,
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
export const getPricing = (): ServicePricing => ({
|
|
||||||
serviceFee: PRICING.SERVICE_FEE,
|
|
||||||
certificateFee: PRICING.CERTIFICATE_FEE,
|
|
||||||
currency: PRICING.CURRENCY,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const calculateTotal = (hasCertificate: boolean): number => {
|
|
||||||
const base = PRICING.SERVICE_FEE;
|
|
||||||
return hasCertificate ? base + PRICING.CERTIFICATE_FEE : base;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toTiyin = (uzs: number): number => uzs * PRICING.TIYIN_MULTIPLIER;
|
|
||||||
|
|
||||||
export const formatPrice = (amount: number, currency: string): string =>
|
|
||||||
`${amount.toLocaleString('uz-UZ')} ${currency}`;
|
|
||||||
|
|
||||||
// ─── Order ID Generator ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const generateOrderId = (): string => {
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const random = Math.random().toString(36).slice(2, 8).toUpperCase();
|
|
||||||
return `ORDER-${timestamp}-${random}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Payme API ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends payment details to the backend, which creates a Payme transaction
|
|
||||||
* and returns a redirect URL to the Payme checkout page.
|
|
||||||
*/
|
|
||||||
export const createPaymePayment = async (
|
|
||||||
request: PaymePaymentRequest,
|
|
||||||
): Promise<PaymePaymentResponse> => {
|
|
||||||
const response = await fetch(PAYME_CONFIG.API_ENDPOINT, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
amount: request.amount, // in tiyin
|
|
||||||
order_id: request.orderId,
|
|
||||||
description: request.description,
|
|
||||||
return_url: request.returnUrl,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(
|
|
||||||
(errorBody as { message?: string }).message ??
|
|
||||||
`Payment request failed with status ${response.status}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await response.json()) as PaymePaymentResponse;
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects the user to the Payme checkout page.
|
|
||||||
*/
|
|
||||||
export const redirectToPayme = (redirectUrl: string): void => {
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user