update flywire

This commit is contained in:
LittleBoy 2024-06-22 20:56:29 +08:00
parent f899760a86
commit 9095b4d9b1
12 changed files with 152 additions and 80 deletions

View File

@ -173,7 +173,8 @@ body #root .dashboard-layout{
.dashboard-menu-container {
padding: var(--dashboard-layout-padding, 15px);
position: sticky;
top:var(--dashboard-header-height, 50px);
.nav-item {
padding: 15px;
display: flex;

View File

@ -130,4 +130,14 @@ export const IconBillType = ({style}: IconProps) => {
</g>
</svg>
)
}
}
export const IconLoading = ({size}:{size?:string|number})=>(<svg xmlns="http://www.w3.org/2000/svg" style={{
margin: 'auto', display: 'block',...(size?{fontSize:size}:{})
}} width="1em" height="1em" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" r="30" stroke="#6a6a6a" strokeWidth="6" fill="none"></circle>
<circle cx="50" cy="50" r="30" stroke="#aaaaaa" strokeWidth="6" strokeLinecap="round" fill="none">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;180 50 50;720 50 50" keyTimes="0;0.5;1"></animateTransform>
<animate attributeName="stroke-dasharray" repeatCount="indefinite" dur="1s" values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882" keyTimes="0;0.5;1"></animate>
</circle>
</svg>)

View File

@ -87,6 +87,8 @@
"bill_error": "Bills to be paid do not exist or are overdue",
"charge": "Charge",
"confirm_pay": "CONFIRM PAYMENT",
"query_pay_status": "Check payment status...",
"returnBack": "Return Payment System",
"text_canceled": "Payment has been cancelled",
"text_failed": "Payment Failure",
"text_success": "Success",

View File

@ -87,6 +87,8 @@
"bill_error": "待支付账单不存在或已过期",
"charge": "手续费",
"confirm_pay": "确认支付",
"query_pay_status": "查询支付状态...",
"returnBack": "返回Payment System",
"text_canceled": "支付已取消",
"text_failed": "支付失败",
"text_success": "支付成功",

View File

@ -87,6 +87,8 @@
"bill_error": "待支付帳單不存在或已過期",
"charge": "手續費",
"confirm_pay": "確認付款",
"query_pay_status": "查詢支付狀態...",
"returnBack": "返回Payment System",
"text_canceled": "付款已取消",
"text_failed": "付款失敗",
"text_success": "付款成功",

View File

@ -12,6 +12,13 @@ import {BillStatus} from "@/service/types.ts";
import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts";
import useAuth from "@/hooks/useAuth.ts";
const DownloadButton = ({bill,text}: { bill: BillModel;text:string }) => {
const {loading: downloading, downloadPDF} = useDownloadReceiptPDF()
return (<Button
onClick={() => downloadPDF(bill)} size={'small'} theme={'solid'}
type={'primary'} loading={downloading}>{text}</Button>)
}
const BillQuery = () => {
const {user} = useAuth();
const [showBill, setShowBill] = useState<BillModel>()
@ -31,7 +38,6 @@ const BillQuery = () => {
});
}
})
const {loading: downloading, downloadPDF} = useDownloadReceiptPDF()
const {t} = useTranslation()
const onConfirmCancel = (bill: BillModel) => {
@ -56,10 +62,7 @@ const BillQuery = () => {
{AppMode == 'development' && <a href={`/pay?bill=${bill.id}`} target={'_blank'}></a>}
</>}
{
bill.status == BillStatus.PAID
&& <Button
onClick={() => downloadPDF(bill)} size={'small'} theme={'solid'}
type={'primary'} loading={downloading}>{t('bill.download_receipt')}</Button>
bill.status == BillStatus.PAID && <DownloadButton bill={bill} text={t('bill.download_receipt')}/>
}
</Space>)
}

View File

@ -1,43 +1,32 @@
import styles from "@/pages/pay/pay.module.less";
import React, {useMemo} from "react";
import React, {useEffect, useState} from "react";
import {getAppUrl} from "@/hooks/useAppUrl.ts";
import {useTranslation} from "react-i18next";
import {IconLoading} from "@/components/icons";
import {getFlywirePayUrl} from "@/service/api/bill.ts";
// 支付费用配置
const PayFeeTypeConfig: {
[key: string]: string
} = {
'CAUTION FEE': 'items[caution_fee]', // caution_fee
'STUDENT UNION FEE': 'items[student_union_fee]', // student_union_fee
'TUITION FEE': 'items[tuition_fee]', // tuition_fee
'CREDIT POINT': 'items[credit_point]', // credit_point
'APPLICATION FEE': 'items[application_fee]', // application_fee
type StartFlyWireProps = {
open?: boolean;
bill: BillModel
}
function getPaymentFees(bill: BillModel) {
// 根据bill details的账单类型设置支付费用
return bill.details.map(it => {
return (PayFeeTypeConfig[it.bill_type] ?? 'items[other_fee]') + `=${Number(it.amount) * 100}`
})
}
export const StartFlyWire: React.FC<{ bill: BillModel }> = ({bill}) => {
const callbackUrl = getAppUrl() + "/pay/success?from=flywire";
export const StartFlyWire: React.FC<StartFlyWireProps> = ({bill, open}) => {
const callbackUrl = getAppUrl() + "/pay/success?from=FLYWIRE&bill=" + bill.id;
const {t} = useTranslation()
const redirectUrl = useMemo(() => {
const fees = getPaymentFees(bill) // 支付费用
// flywire 支付跳转连接
return `${AppConfig.FIY_WIRE_GATEWAY}?provider=HAI&payment_destination=chuhaicollege`
+ `&${fees.join('&')}`
+ `&student_last_name=${encodeURIComponent(bill.student_english_name)}`
+ `&student_id=${encodeURIComponent(bill.student_number || bill.application_number || '')}`
+ `&student_email=${encodeURIComponent(bill.student_email)}`
+ `&callback_url=${encodeURIComponent(callbackUrl)}`
+ `&callback_id=${encodeURIComponent(bill.merchant_ref || '')}`
const [loading, setLoading] = useState<boolean>(false)
const [redirectUrl, setRedirectUrl] = useState<string>()
useEffect(() => {
if (bill) {
setLoading(true)
getFlywirePayUrl(bill.id, callbackUrl, t(`pay.returnBack`)).then(res => {
setRedirectUrl(res.url)
}).catch(console.error).finally(() => setLoading(false))
}
}, [bill])
return (<a href={redirectUrl} className={styles.btnConfirm}>
{t('pay.confirm_pay')}
</a>)
return (<>
{open ? (!loading ? <a href={redirectUrl} className={styles.btnConfirm}>
{t('pay.confirm_pay')}
</a> : <button className={styles.btnConfirm} style={{fontSize: 26}}><IconLoading/></button>):<></>}
</>)
}

View File

@ -93,7 +93,8 @@ const PayIndex = () => {
<div className={styles.payConfirm}>
<div className="student-email">Your Email: {bill.student_email}</div>
<div className="pay-submit">
{ payChannel == 'asia_pay' ? <StartAsiaPay bill={bill} /> : <StartFlyWire bill={bill} />}
{ payChannel == 'asia_pay' && <StartAsiaPay bill={bill} />}
<StartFlyWire bill={bill} open={payChannel == 'flywire'} />
</div>
</div>
</> : <></>}

View File

@ -9,7 +9,14 @@
width: 600px;
transition: width 0.1s;
}
.payLoading{
padding: 50px 0;
text-align: center;
}
.loadingText{
font-size: 32px;
margin-top: 20px;
}
.payAmount {
color: #fff;
background-color: #50ADA7;

View File

@ -9,11 +9,14 @@ import {useDownloadReceiptPDF} from "@/service/generate-pdf.ts";
import {useEffect} from "react";
import {useSetState} from "ahooks";
import {FailIcon} from "@/assets/images/pay/fail.tsx";
import {updateBillPaymentSuccess} from "@/service/api/bill.ts";
type PayResult = 'fail' | 'success' | 'cancel' | 'error' | string;
import {getBillDetail, updateBillPaymentSuccess} from "@/service/api/bill.ts";
import {BillStatus} from "@/service/types.ts";
import {IconLoading} from "@/components/icons";
type PayResult = 'fail' | 'success' | 'cancel' | 'error' | 'loading' | string;
const QUERY_MAX_COUNT = 50,
QUERY_DELAY = 6000;
const PayIndex = () => {
const {t} = useTranslation()
@ -24,14 +27,48 @@ const PayIndex = () => {
id?: string;
bill?: BillModel
status?: string | null;
}>({});
loading?: boolean;
queryCount: number;
}>({
result: 'loading',
queryCount: 0
});
const param = useParams<{ result: PayResult }>();
const [search] = useSearchParams(); // 参数有: from: asia_spay || flywire
const updateBillSuccess = (billId: number, type: string, ref: string) =>{
const updateBillSuccess = (billId: number, type: string, ref: string) => {
setState({result: 'loading'})
updateBillPaymentSuccess(billId, ref, type).then(bill => {
setState({bill})
}).catch(()=>{
setState({result:'fail'})
setState({
result: 'success',
bill: bill
})
}).catch(() => {
setState({result: 'fail'})
})
}
const checkBillSuccess = (billId: number) => {
setState({
result: 'loading',
queryCount: state.queryCount + 1
})
getBillDetail(Number(billId)).then((bill) => {
// 判断bill状态
if (bill.status != BillStatus.PAID) {
if(state.queryCount > QUERY_MAX_COUNT) {
setState({result: 'fail'})
}else{
// 重试
setTimeout(()=>checkBillSuccess(billId), QUERY_DELAY);
}
return;
}
setState({
result: 'success',
bill: bill
})
}).catch(() => {
})
}
useEffect(() => {
@ -47,8 +84,13 @@ const PayIndex = () => {
return;
}
if (result == 'success') {
updateBillSuccess(Number(bill), from , (from == 'ASIAPAY' ? search.get('Ref')! : search.get('callback_id')!));
if (from == 'ASIAPAY') {
updateBillSuccess(Number(bill), from, search.get('Ref')!);
} else if (from == 'FLYWIRE') {
checkBillSuccess(Number(bill));
}
}
return;
}
setState({result, status})
}, []);
@ -56,33 +98,42 @@ const PayIndex = () => {
return (<div className={styles.container}>
<PayLogo/>
{state.result == 'success' ? <>
<div className={styles.paySuccess}>
<div className={styles.successIcon}>
<SuccessIcon/>
<div className={styles.paySuccess}>
<div className={styles.successIcon}>
<SuccessIcon/>
</div>
<h2 className={`${styles.payText} pay-text`}>
{t('pay.text_success')}
</h2>
</div>
<h2 className={`${styles.payText} pay-text`}>
{t('pay.text_success')}
</h2>
</div>
{state.bill && <div className={styles.payConfirm}>
<div className="student-email">{t('bill.bill_number')}: {state.bill.student_number || state.bill.application_number}</div>
<div className="pay-submit">
<Button
theme={'solid'}
onClick={() => downloadPDF(state.bill!)}
style={{width: 250, marginTop: 20}}>{t('bill.download_receipt')}</Button>
</div>
</div>}
</> : <div className={styles.paySuccess}>
<div className={styles.successIcon} style={{color: '#f05672'}}>
<FailIcon/>
</div>
<h2 className={`${styles.payText} pay-text`}>
{state.result == 'fail' ? t('pay.text_failed') : (
state.result == 'cancel' ? t('pay.text_canceled') : (t('pay.bill_error'))
)}
</h2>
</div>}
{state.bill && <div className={styles.payConfirm}>
<div
className="student-email">{t('bill.bill_number')}: {state.bill.student_number || state.bill.application_number}</div>
<div className="pay-submit">
<Button
theme={'solid'}
onClick={() => downloadPDF(state.bill!)}
style={{width: 250, marginTop: 20}}>{t('bill.download_receipt')}</Button>
</div>
</div>}
</>
: (
state.result == 'loading' ? <div className={styles.payLoading}>
<IconLoading size={70} />
<div className={styles.loadingText}>{t('pay.query_pay_status')}</div>
</div>
: (<div className={styles.paySuccess}>
<div className={styles.successIcon} style={{color: '#f05672'}}>
<FailIcon/>
</div>
<h2 className={`${styles.payText} pay-text`}>
{state.result == 'fail' ? t('pay.text_failed') : (
state.result == 'cancel' ? t('pay.text_canceled') : (t('pay.bill_error'))
)}
</h2>
</div>)
)
}
</div>);
}
export default PayIndex;

View File

@ -42,6 +42,10 @@ export function getAsiaPayData(id: number) {
return get<AsiaPayModel>(`/bills/${id}/asiapay`)
}
export function getFlywirePayUrl(id: number, return_cta: string, return_cta_name: string) {
return post<{ url: string }>(`/bills/${id}/flywire`, {return_cta, return_cta_name})
}
// 作废订单
export function cancelBill(id: number) {
return put(`/bills/${id}/cancel`)
@ -51,6 +55,6 @@ export function confirmBills(bill_ids: number[]) {
return post(`/bills/apply`, {bill_ids})
}
export function updateBillPaymentSuccess(bill_id: number, merchant_ref: string,payment_channel:string) {
return post<BillModel>(`/bills/finish`, {merchant_ref,payment_channel,bill_id})
export function updateBillPaymentSuccess(bill_id: number, merchant_ref: string, payment_channel: string) {
return post<BillModel>(`/bills/finish`, {merchant_ref, payment_channel, bill_id})
}

View File

@ -35,7 +35,7 @@ export function GeneratePdf(bill: BillModel) {
title: 'Programme:',
content: bill.programme_english_name
}, 56)
drawItem(doc, {title: 'Mode of Study:', content: bill.attendance_mode}, 70)
drawItem(doc, {title: 'Mode of Study:', content: bill.attendance_mode}, bill.programme_english_name.length > 70?70:64)
// draw table
autoTable(doc, {
startY: 80,