show refund info in purchase

notify refund pending with accept button on the purchase details
better payto box
This commit is contained in:
Sebastian 2022-05-29 01:23:15 -03:00
parent ced08c502f
commit 029340469a
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
13 changed files with 183 additions and 24 deletions

View File

@ -244,6 +244,11 @@ export interface TransactionPayment extends TransactionCommon {
* Amount pending to be picked up
*/
refundPending: AmountString | undefined;
/**
* Reference to applied refunds
*/
refunds: RefundInfoShort[];
}
export interface OrderShortInfo {
@ -305,6 +310,13 @@ export interface OrderShortInfo {
fulfillmentMessage_i18n?: InternationalizedString;
}
export interface RefundInfoShort {
transactionId: string,
timestamp: TalerProtocolTimestamp,
amountEffective: AmountString,
amountRaw: AmountString,
}
export interface TransactionRefund extends TransactionCommon {
type: TransactionType.Refund;

View File

@ -799,6 +799,15 @@ export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
.property("talerRefundUri", codecForString())
.build("ApplyRefundRequest");
export interface ApplyRefundFromPurchaseIdRequest {
purchaseId: string;
}
export const codecForApplyRefundFromPurchaseIdRequest = (): Codec<ApplyRefundFromPurchaseIdRequest> =>
buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
.property("purchaseId", codecForString())
.build("ApplyRefundFromPurchaseIdRequest");
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
restrictAge?: number;

View File

@ -573,7 +573,7 @@ export async function applyRefund(
throw Error("invalid refund URI");
}
let purchase = await ws.db
const purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))
@ -590,7 +590,15 @@ export async function applyRefund(
);
}
const proposalId = purchase.proposalId;
return applyRefundFromPurchaseId(ws, purchase.proposalId)
}
export async function applyRefundFromPurchaseId(
ws: InternalWalletState,
proposalId: string,
): Promise<ApplyRefundResponse> {
logger.trace("applying refund for purchase", proposalId);
logger.info("processing purchase for refund");
const success = await ws.db
@ -620,7 +628,7 @@ export async function applyRefund(
});
}
purchase = await ws.db
const purchase = await ws.db
.mktx((x) => ({
purchases: x.purchases,
}))

View File

@ -24,6 +24,7 @@ import {
Logger,
OrderShortInfo,
PaymentStatus,
RefundInfoShort,
Transaction,
TransactionsRequest,
TransactionsResponse,
@ -306,6 +307,7 @@ export async function getTransactions(
let totalRefundRaw = Amounts.getZero(contractData.amount.currency);
let totalRefundEffective = Amounts.getZero(contractData.amount.currency);
const refunds: RefundInfoShort[] = []
for (const groupKey of refundGroupKeys.values()) {
const refundTombstoneId = makeEventId(
@ -345,6 +347,13 @@ export async function getTransactions(
refund.totalRefreshCostBound,
).amount,
).amount;
refunds.push({
transactionId: refundTransactionId,
timestamp: r0.obtainedTime,
amountEffective: Amounts.stringify(amountEffective),
amountRaw: Amounts.stringify(amountRaw),
})
}
}
if (!r0) {
@ -353,7 +362,6 @@ export async function getTransactions(
totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount;
totalRefundEffective = Amounts.add(totalRefundEffective, amountEffective).amount;
transactions.push({
type: TransactionType.Refund,
info,
@ -382,10 +390,11 @@ export async function getTransactions(
pending:
!pr.timestampFirstSuccessfulPay &&
pr.abortStatus === AbortStatus.None,
refunds,
timestamp: pr.timestampAccept,
transactionId: paymentTransactionId,
proposalId: pr.proposalId,
info: info,
info,
frozen: pr.payFrozen ?? false,
...(err ? { error: err } : {}),
});

View File

@ -33,6 +33,7 @@ import {
codecForAcceptTipRequest,
codecForAddExchangeRequest,
codecForAny,
codecForApplyRefundFromPurchaseIdRequest,
codecForApplyRefundRequest,
codecForConfirmPayRequest,
codecForCreateDepositGroupRequest,
@ -145,6 +146,7 @@ import {
import {
abortFailedPayWithRefund,
applyRefund,
applyRefundFromPurchaseId,
prepareRefund,
processPurchaseQueryRefund
} from "./operations/refund.js";
@ -839,6 +841,10 @@ async function dispatchRequestInternal(
const req = codecForApplyRefundRequest().decode(payload);
return await applyRefund(ws, req.talerRefundUri);
}
case "applyRefundFromPurchaseId": {
const req = codecForApplyRefundFromPurchaseIdRequest().decode(payload);
return await applyRefundFromPurchaseId(ws, req.purchaseId);
}
case "acceptBankIntegratedWithdrawal": {
const req =
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);

View File

@ -92,6 +92,7 @@ const CollasibleBox = styled.div`
}
`;
import arrowDown from "../svg/chevron-down.svg";
import { useTranslationContext } from "../context/translation.js";
export function PartCollapsible({ text, title, big, showSign }: Props): VNode {
const Text = big ? ExtraLargeText : LargeText;
@ -137,27 +138,37 @@ interface PropsPayto {
}
export function PartPayto({ payto, kind, big }: PropsPayto): VNode {
const Text = big ? ExtraLargeText : LargeText;
let text: string | undefined = undefined;
let text: VNode | undefined = undefined;
let title = "";
const { i18n } = useTranslationContext();
if (payto.isKnown) {
if (payto.targetType === "x-taler-bank") {
text = payto.account;
title = "Bank account";
text = <Fragment>{payto.account}</Fragment>;
title = i18n.str`Bank account`;
} else if (payto.targetType === "bitcoin") {
text = payto.targetPath;
title = "Bitcoin addr";
text =
payto.segwitAddrs && payto.segwitAddrs.length > 0 ? (
<ul>
<li>{payto.targetPath}</li>
<li>{payto.segwitAddrs[0]}</li>
<li>{payto.segwitAddrs[1]}</li>
</ul>
) : (
<Fragment>{payto.targetPath}</Fragment>
);
title = i18n.str`Bitcoin address`;
} else if (payto.targetType === "iban") {
text = payto.targetPath;
title = "IBAN";
text = <Fragment>{payto.targetPath}</Fragment>;
title = i18n.str`IBAN`;
}
}
if (!text) {
text = stringifyPaytoUri(payto);
text = <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
title = "Payto URI";
}
return (
<div style={{ margin: "1em" }}>
<SmallLightText style={{ margin: ".5em" }}>{title}</SmallLightText>
<SmallBoldText>{title}</SmallBoldText>
<Text
style={{
color:

View File

@ -207,7 +207,7 @@ function TransactionAmount(props: TransactionAmountProps): VNode {
>
<ExtraLargeText>
{sign}
{Amounts.stringifyValue(props.amount)}
{Amounts.stringifyValue(props.amount, 2)}
</ExtraLargeText>
{props.pending && (
<div>

View File

@ -78,6 +78,7 @@ const exampleData = {
summary: "the summary",
fulfillmentMessage: "",
},
refunds: [],
refundPending: undefined,
totalRefundEffective: "USD:0",
totalRefundRaw: "USD:0",

View File

@ -193,7 +193,7 @@ export function HistoryView({
margin: 8,
}}
>
{Amounts.stringifyValue(currencyAmount)}
{Amounts.stringifyValue(currencyAmount, 2)}
</CenteredBoldText>
)}
</div>

View File

@ -45,7 +45,7 @@ export const TalerBank = createExample(TestedComponent, {
export const IBAN = createExample(TestedComponent, {
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
paytoURI: parsePaytoUri(
"payto://iban/ASDQWEASDZXCASDQWE?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
"payto://iban/ES8877998399652238?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
),
amount: {
currency: "USD",

View File

@ -93,6 +93,7 @@ const exampleData = {
// address_lines: [""],
// },
},
refunds: [],
refundPending: undefined,
totalRefundEffective: "KUDOS:0",
totalRefundRaw: "KUDOS:0",
@ -199,7 +200,7 @@ export const WithdrawPendingManual = createExample(TestedComponent, () => ({
...exampleData.withdraw,
withdrawalDetails: {
type: WithdrawalType.ManualTransfer,
exchangePaytoUris: ["payto://iban/asdasdasd"],
exchangePaytoUris: ["payto://iban/ES8877998399652238"],
reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
} as WithdrawalDetails,
pending: true,
@ -254,6 +255,14 @@ export const PaymentWithRefund = createExample(TestedComponent, {
amountRaw: "KUDOS:12",
totalRefundEffective: "KUDOS:1",
totalRefundRaw: "KUDOS:1",
refunds: [
{
transactionId: "1123123",
amountRaw: "KUDOS:1",
amountEffective: "KUDOS:1",
timestamp: TalerProtocolTimestamp.fromSeconds(1546546544),
},
],
},
});
@ -410,6 +419,25 @@ export const PaymentWithLongSummary = createExample(TestedComponent, {
export const Deposit = createExample(TestedComponent, {
transaction: exampleData.deposit,
});
export const DepositTalerBank = createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
targetPaytoUri: "payto://x-taler-bank/bank.demo.taler.net/Exchange",
},
});
export const DepositBitcoin = createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
targetPaytoUri:
"payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
},
});
export const DepositIBAN = createExample(TestedComponent, {
transaction: {
...exampleData.deposit,
targetPaytoUri: "payto://iban/ES8877998399652238",
},
});
export const DepositError = createExample(TestedComponent, {
transaction: {

View File

@ -22,6 +22,8 @@ import {
NotificationType,
parsePaytoUri,
parsePayUri,
PaytoUri,
stringifyPaytoUri,
TalerProtocolTimestamp,
Transaction,
TransactionDeposit,
@ -50,6 +52,7 @@ import {
ButtonDestructive,
ButtonPrimary,
CenteredDialog,
HistoryRow,
InfoBox,
ListOfProducts,
Overlay,
@ -83,7 +86,7 @@ async function getTransaction(tid: string): Promise<Transaction> {
export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
const { i18n } = useTranslationContext();
const state = useAsyncAsHook(() => getTransaction(tid));
const state = useAsyncAsHook(() => getTransaction(tid), [tid]);
useEffect(() => {
wxApi.onUpdateNotification([NotificationType.WithdrawGroupFinished], () => {
@ -119,6 +122,7 @@ export function TransactionPage({ tid, goToWalletHistory }: Props): VNode {
onRetry={() =>
wxApi.retryTransaction(tid).then(() => goToWalletHistory(currency))
}
onRefund={(id) => wxApi.applyRefundFromPurchaseId(id)}
onBack={() => goToWalletHistory(currency)}
/>
);
@ -128,6 +132,7 @@ export interface WalletTransactionProps {
transaction: Transaction;
onDelete: () => void;
onRetry: () => void;
onRefund: (id: string) => void;
onBack: () => void;
}
@ -143,7 +148,7 @@ export function TransactionView({
transaction,
onDelete,
onRetry,
onBack,
onRefund,
}: WalletTransactionProps): VNode {
const [confirmBeforeForget, setConfirmBeforeForget] = useState(false);
@ -334,6 +339,40 @@ export function TransactionView({
)}
</Header>
<br />
{transaction.refunds.length > 0 ? (
<Part
title={<i18n.Translate>Refunds</i18n.Translate>}
text={
<table>
{transaction.refunds.map((r, i) => {
return (
<tr key={i}>
<td>
{<Amount value={r.amountEffective} />}{" "}
<a
href={Pages.balance_transaction.replace(
":tid",
r.transactionId,
)}
>
was refunded
</a>{" "}
on{" "}
{
<Time
timestamp={AbsoluteTime.fromTimestamp(r.timestamp)}
format="dd MMMM yyyy"
/>
}
</td>
</tr>
);
})}
</table>
}
kind="neutral"
/>
) : undefined}
{pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
<InfoBox>
<i18n.Translate>
@ -348,7 +387,7 @@ export function TransactionView({
<div>
<div />
<div>
<ButtonPrimary>
<ButtonPrimary onClick={() => onRefund(transaction.proposalId)}>
<i18n.Translate>Accept</i18n.Translate>
</ButtonPrimary>
</div>
@ -385,9 +424,9 @@ export function TransactionView({
total={total}
kind="negative"
>
{transaction.targetPaytoUri}
{!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />}
</Header>
{payto && <PartPayto big payto={payto} kind="neutral" />}
{payto && <PartPayto payto={payto} kind="neutral" />}
<Part
title={<i18n.Translate>Details</i18n.Translate>}
text={<DepositDetails transaction={transaction} />}
@ -669,7 +708,7 @@ function PurchaseDetails({
<tr>
<td>Refunded</td>
<td>
<Amount value={transaction.totalRefundEffective} />
<Amount value={transaction.totalRefundRaw} />
</td>
</tr>
)}
@ -988,3 +1027,30 @@ function Header({
</div>
);
}
function NicePayto({ payto }: { payto: PaytoUri }): VNode {
if (payto.isKnown) {
switch (payto.targetType) {
case "bitcoin": {
return <div>{payto.targetPath.substring(0, 20)}...</div>;
}
case "x-taler-bank": {
const url = new URL("/", `https://${payto.host}`);
return (
<Fragment>
<div>{payto.account}</div>
<SmallLightText>
<a href={url.href} target="_bank" rel="noreferrer">
{url.toString()}
</a>
</SmallLightText>
</Fragment>
);
}
case "iban": {
return <div>{payto.targetPath.substring(0, 20)}</div>;
}
}
}
return <Fragment>{stringifyPaytoUri(payto)}</Fragment>;
}

View File

@ -311,6 +311,15 @@ export function applyRefund(
return callBackend("applyRefund", { talerRefundUri });
}
/**
* Do refund for purchase.
*/
export function applyRefundFromPurchaseId(
purchaseId: string,
): Promise<ApplyRefundResponse> {
return callBackend("applyRefundFromPurchaseId", { purchaseId });
}
/**
* Get details about a pay operation.
*/