payme price calculation update on modal

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-04-06 10:38:17 +05:00
parent d70360841a
commit c01f3917d2
17 changed files with 73 additions and 126 deletions

View File

@@ -1,16 +1,5 @@
// ─── Domain Types ──────────────────────────────────────────────────────────────
export interface ServicePricing {
serviceFee: number;
certificateFee: number;
currency: string;
}
export interface OrderSummary {
hasCertificate: boolean;
pricing: ServicePricing;
}
export interface PaymePaymentRequest {
amount: number; // in tiyin (1 UZS = 100 tiyin)
orderId: string;
@@ -26,20 +15,21 @@ export interface PaymePaymentResponse {
export type PaymentStatus = 'idle' | 'loading' | 'success' | 'error';
// ─── Component Props ───────────────────────────────────────────────────────────
export interface PriceCalculate {
service_fee: number;
discount?: number;
total_price: number;
currency: string;
}
export interface PaymentModalProps {
isOpen: boolean;
onClose: () => void;
hasCertificate: boolean;
price: PriceCalculate;
onConfirmPayment: () => void;
isLoading: boolean;
}
export interface PriceSummaryProps {
hasCertificate: boolean;
pricing: ServicePricing;
}
export interface PaymeButtonProps {
amount: number;
orderId: string;

View 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;
};

View File

@@ -1,7 +1,6 @@
'use client';
import React, { useEffect, useRef } from 'react';
import { PaymentModalProps } from '../lib/types';
import { getPricing } from '../lib/utils';
import { PriceSummary } from './Pricesummary';
import { PaymeButton } from './Paymebutton';
import { useTranslations } from 'next-intl';
@@ -85,12 +84,11 @@ const SecurityBadge: React.FC<{ securityText: string }> = ({
export const PaymentModal: React.FC<PaymentModalProps> = ({
isOpen,
onClose,
hasCertificate,
price,
onConfirmPayment,
isLoading,
}) => {
const dialogRef = useRef<HTMLDivElement>(null);
const pricing = getPricing();
const status = isLoading ? 'loading' : 'idle';
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">
{t('orderSummary')}
</h3>
<PriceSummary hasCertificate={hasCertificate} pricing={pricing} />
<PriceSummary priceCalculate={price} />
</div>
{/* 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">
<svg
width="15"
@@ -191,7 +188,6 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({
</svg>
<span>{t('certificateIncluded')}</span>
</div>
)}
{/* Payment method label */}
<div>

View File

@@ -1,8 +1,8 @@
'use client';
import React from 'react';
import { formatPrice } from '../lib/utils';
import { PriceSummaryProps } from '../lib/types';
import { useTranslations } from 'next-intl';
import { PriceCalculate } from '../lib/types';
// ─── Price Row ─────────────────────────────────────────────────────────────────
@@ -47,34 +47,33 @@ const PriceRow: React.FC<PriceRowProps> = ({
// ─── Price Summary ─────────────────────────────────────────────────────────────
export const PriceSummary: React.FC<PriceSummaryProps> = ({
hasCertificate,
pricing,
export const PriceSummary = ({
priceCalculate,
}: {
priceCalculate: PriceCalculate;
}) => {
console.log(hasCertificate);
const total = 41200;
const t = useTranslations('Payment');
return (
<div className="rounded-xl bg-slate-50 border border-slate-100 px-5 py-2 space-y-0">
<PriceRow
label={t('serviceFee')}
amount={41200}
currency={pricing.currency}
amount={priceCalculate.service_fee || 0}
currency={priceCalculate.currency}
/>
{/* {hasCertificate && (
{priceCalculate.discount && (
<PriceRow
label={t('certificateLabel')}
amount={pricing.certificateFee}
currency={pricing.currency}
amount={priceCalculate.discount}
currency={priceCalculate.currency}
/>
)} */}
)}
<PriceRow
label={t('total')}
amount={total}
currency={pricing.currency}
amount={priceCalculate.total_price}
currency={priceCalculate.currency}
highlight
/>
</div>

View File

@@ -7,7 +7,7 @@ import { useParams } from 'next/navigation';
import { links } from '@/shared/request/links';
import { apiRequest } from '@/shared/request/apiRequest';
import PaymentStatus from './paidStatus';
import Sertifikat from './ui/sertificate/sertifikat';
import Sertifikat from '@/features/modals/sertificateModal/sertifikat';
// ── Types ────────────────────────────────────────────────────────────────────

View File

@@ -10,9 +10,9 @@ import {
StatusBanner,
} from './Plagiraismui';
import { usePlagiarismForm } from '../lib/usePlagiraism';
import { PaymentModal } from '@/widgets/paymentModal/ui/Paymentmodal';
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 = `
w-full px-3.5 py-3.5 text-[14px] text-slate-800
@@ -218,7 +218,12 @@ export function PlagiarismCheckForm() {
<PaymentModal
isOpen={isPaymentOpen}
onClose={() => setIsPaymentOpen(false)}
hasCertificate={form.certificate}
price={{
service_fee: 41200,
discount: 5200,
total_price: 36000,
currency: 'UZS',
}}
onConfirmPayment={handleSubmit}
isLoading={isLoading}
/>

View File

@@ -1,5 +1,7 @@
// ─── Domain Types ──────────────────────────────────────────────────────────────
import { PriceCalculate } from '@/features/modals/paymentModal/lib/types';
export type CheckResult = 'clean' | 'plagiarism_found' | 'pending' | 'failed';
export interface DocumentData {
@@ -13,6 +15,7 @@ export interface DocumentData {
results: [];
state: 'paid' | 'unpaid';
order_id: number;
price_calculation?: PriceCalculate;
}
export interface PlagiarismCheckDetail extends DocumentData {

View File

@@ -6,11 +6,11 @@ import { formatDate } from '../lib/utils';
import { useRouter } from '@/shared/config/i18n/navigation';
import { useUserPlagiatStore } from '@/shared/zustand/user';
import PaymentStatus from '@/widgets/detail/paidStatus';
import { PaymentModal } from '@/widgets/paymentModal/ui/Paymentmodal';
import { useMutation } from '@tanstack/react-query';
import { apiRequest } from '@/shared/request/apiRequest';
import { links } from '@/shared/request/links';
import { toast } from 'react-toastify';
import { PaymentModal } from '@/features/modals/paymentModal/ui/Paymentmodal';
export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
const router = useRouter();
@@ -160,7 +160,12 @@ export const HistoryTableRow: React.FC<HistoryTableRowProps> = ({ item }) => {
<PaymentModal
isOpen={isPaymentOpen}
onClose={() => setIsPaymentOpen(false)}
hasCertificate={false}
price={{
service_fee: 41200,
discount: 5200,
total_price: 36000,
currency: 'UZS',
}}
onConfirmPayment={() => {
handleSubmit({ document_id: Number(item.order_id) });
}}

View File

@@ -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;
};