feat: awaiting refund
This commit is contained in:
parent
c02dbc833b
commit
e4ea201943
@ -562,8 +562,8 @@ export interface MerchantAbortPayRefundDetails {
|
|||||||
refund_amount: string;
|
refund_amount: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for the refund.
|
* Fee for the refund.
|
||||||
*/
|
*/
|
||||||
refund_fee: string;
|
refund_fee: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -888,18 +888,18 @@ export type BlindedDenominationSignature =
|
|||||||
| RsaBlindedDenominationSignature
|
| RsaBlindedDenominationSignature
|
||||||
| CSBlindedDenominationSignature;
|
| CSBlindedDenominationSignature;
|
||||||
|
|
||||||
export const codecForBlindedDenominationSignature = () =>
|
|
||||||
buildCodecForUnion<BlindedDenominationSignature>()
|
|
||||||
.discriminateOn("cipher")
|
|
||||||
.alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
|
|
||||||
.build("BlindedDenominationSignature");
|
|
||||||
|
|
||||||
export const codecForRsaBlindedDenominationSignature = () =>
|
export const codecForRsaBlindedDenominationSignature = () =>
|
||||||
buildCodecForObject<RsaBlindedDenominationSignature>()
|
buildCodecForObject<RsaBlindedDenominationSignature>()
|
||||||
.property("cipher", codecForConstString(DenomKeyType.Rsa))
|
.property("cipher", codecForConstString(DenomKeyType.Rsa))
|
||||||
.property("blinded_rsa_signature", codecForString())
|
.property("blinded_rsa_signature", codecForString())
|
||||||
.build("RsaBlindedDenominationSignature");
|
.build("RsaBlindedDenominationSignature");
|
||||||
|
|
||||||
|
export const codecForBlindedDenominationSignature = () =>
|
||||||
|
buildCodecForUnion<BlindedDenominationSignature>()
|
||||||
|
.discriminateOn("cipher")
|
||||||
|
.alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
|
||||||
|
.build("BlindedDenominationSignature");
|
||||||
|
|
||||||
export class WithdrawResponse {
|
export class WithdrawResponse {
|
||||||
ev_sig: BlindedDenominationSignature;
|
ev_sig: BlindedDenominationSignature;
|
||||||
}
|
}
|
||||||
@ -1024,15 +1024,17 @@ export interface ExchangeRevealResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MerchantOrderStatusPaid {
|
interface MerchantOrderStatusPaid {
|
||||||
/**
|
// Was the payment refunded (even partially, via refund or abort)?
|
||||||
* Was the payment refunded (even partially, via refund or abort)?
|
|
||||||
*/
|
|
||||||
refunded: boolean;
|
refunded: boolean;
|
||||||
|
|
||||||
/**
|
// Is any amount of the refund still waiting to be picked up (even partially)?
|
||||||
* Amount that was refunded in total.
|
refund_pending: boolean;
|
||||||
*/
|
|
||||||
|
// Amount that was refunded in total.
|
||||||
refund_amount: AmountString;
|
refund_amount: AmountString;
|
||||||
|
|
||||||
|
// Amount that already taken by the wallet.
|
||||||
|
refund_taken: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MerchantOrderRefundResponse {
|
interface MerchantOrderRefundResponse {
|
||||||
@ -1528,6 +1530,8 @@ export const codecForMerchantOrderStatusPaid =
|
|||||||
(): Codec<MerchantOrderStatusPaid> =>
|
(): Codec<MerchantOrderStatusPaid> =>
|
||||||
buildCodecForObject<MerchantOrderStatusPaid>()
|
buildCodecForObject<MerchantOrderStatusPaid>()
|
||||||
.property("refund_amount", codecForString())
|
.property("refund_amount", codecForString())
|
||||||
|
.property("refund_taken", codecForString())
|
||||||
|
.property("refund_pending", codecForBoolean())
|
||||||
.property("refunded", codecForBoolean())
|
.property("refunded", codecForBoolean())
|
||||||
.build("MerchantOrderStatusPaid");
|
.build("MerchantOrderStatusPaid");
|
||||||
|
|
||||||
|
@ -228,6 +228,21 @@ export interface TransactionPayment extends TransactionCommon {
|
|||||||
* Amount that was paid, including deposit, wire and refresh fees.
|
* Amount that was paid, including deposit, wire and refresh fees.
|
||||||
*/
|
*/
|
||||||
amountEffective: AmountString;
|
amountEffective: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that has been refunded by the merchant
|
||||||
|
*/
|
||||||
|
totalRefundRaw: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount will be added to the wallet's balance after fees and refreshing
|
||||||
|
*/
|
||||||
|
totalRefundEffective: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount pending to be picked up
|
||||||
|
*/
|
||||||
|
refundPending: AmountString | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderShortInfo {
|
export interface OrderShortInfo {
|
||||||
@ -287,6 +302,11 @@ export interface TransactionRefund extends TransactionCommon {
|
|||||||
// Additional information about the refunded payment
|
// Additional information about the refunded payment
|
||||||
info: OrderShortInfo;
|
info: OrderShortInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount pending to be picked up
|
||||||
|
*/
|
||||||
|
refundPending: AmountString | undefined;
|
||||||
|
|
||||||
// Amount that has been refunded by the merchant
|
// Amount that has been refunded by the merchant
|
||||||
amountRaw: AmountString;
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
@ -279,11 +279,11 @@ export class ReturnCoinsRequest {
|
|||||||
export interface PrepareRefundResult {
|
export interface PrepareRefundResult {
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
|
|
||||||
applied: number;
|
effectivePaid: AmountString;
|
||||||
failed: number;
|
gone: AmountString;
|
||||||
total: number;
|
granted: AmountString;
|
||||||
|
pending: boolean;
|
||||||
amountEffectivePaid: AmountString;
|
awaiting: AmountString;
|
||||||
|
|
||||||
info: OrderShortInfo;
|
info: OrderShortInfo;
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,8 @@ import {
|
|||||||
EddsaPublicKeyString,
|
EddsaPublicKeyString,
|
||||||
codecForAmountString,
|
codecForAmountString,
|
||||||
TalerProtocolDuration,
|
TalerProtocolDuration,
|
||||||
|
codecForTimestamp,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
export interface PostOrderRequest {
|
export interface PostOrderRequest {
|
||||||
@ -80,6 +82,15 @@ export const codecForPostOrderResponse = (): Codec<PostOrderResponse> =>
|
|||||||
.property("token", codecOptional(codecForString()))
|
.property("token", codecOptional(codecForString()))
|
||||||
.build("PostOrderResponse");
|
.build("PostOrderResponse");
|
||||||
|
|
||||||
|
|
||||||
|
export const codecForRefundDetails = (): Codec<RefundDetails> =>
|
||||||
|
buildCodecForObject<RefundDetails>()
|
||||||
|
.property("reason", codecForString())
|
||||||
|
.property("pending", codecForBoolean())
|
||||||
|
.property("amount", codecForString())
|
||||||
|
.property("timestamp", codecForTimestamp)
|
||||||
|
.build("PostOrderResponse");
|
||||||
|
|
||||||
export const codecForCheckPaymentPaidResponse =
|
export const codecForCheckPaymentPaidResponse =
|
||||||
(): Codec<CheckPaymentPaidResponse> =>
|
(): Codec<CheckPaymentPaidResponse> =>
|
||||||
buildCodecForObject<CheckPaymentPaidResponse>()
|
buildCodecForObject<CheckPaymentPaidResponse>()
|
||||||
@ -200,7 +211,10 @@ export interface RefundDetails {
|
|||||||
reason: string;
|
reason: string;
|
||||||
|
|
||||||
// when was the refund approved
|
// when was the refund approved
|
||||||
timestamp: AbsoluteTime;
|
timestamp: TalerProtocolTimestamp;
|
||||||
|
|
||||||
|
// has not been taken yet
|
||||||
|
pending: boolean;
|
||||||
|
|
||||||
// Total amount that was refunded (minus a refund fee).
|
// Total amount that was refunded (minus a refund fee).
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
|
@ -1288,6 +1288,12 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
autoRefundDeadline: TalerProtocolTimestamp | undefined;
|
autoRefundDeadline: TalerProtocolTimestamp | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How much merchant has refund to be taken but the wallet
|
||||||
|
* did not picked up yet
|
||||||
|
*/
|
||||||
|
refundAwaiting: AmountJson | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the payment frozen? I.e. did we encounter
|
* Is the payment frozen? I.e. did we encounter
|
||||||
* an error where it doesn't make sense to retry.
|
* an error where it doesn't make sense to retry.
|
||||||
|
@ -345,7 +345,7 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
const denomPubHash =
|
const denomPubHash =
|
||||||
cryptoComp.rsaDenomPubToHash[
|
cryptoComp.rsaDenomPubToHash[
|
||||||
backupDenomination.denom_pub.rsa_public_key
|
backupDenomination.denom_pub.rsa_public_key
|
||||||
];
|
];
|
||||||
checkLogicInvariant(!!denomPubHash);
|
checkLogicInvariant(!!denomPubHash);
|
||||||
const existingDenom = await tx.denominations.get([
|
const existingDenom = await tx.denominations.get([
|
||||||
@ -560,7 +560,7 @@ export async function importBackup(
|
|||||||
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||||
const contractTermsHash =
|
const contractTermsHash =
|
||||||
cryptoComp.proposalIdToContractTermsHash[
|
cryptoComp.proposalIdToContractTermsHash[
|
||||||
backupProposal.proposal_id
|
backupProposal.proposal_id
|
||||||
];
|
];
|
||||||
let maxWireFee: AmountJson;
|
let maxWireFee: AmountJson;
|
||||||
if (parsedContractTerms.max_wire_fee) {
|
if (parsedContractTerms.max_wire_fee) {
|
||||||
@ -704,7 +704,7 @@ export async function importBackup(
|
|||||||
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||||
const contractTermsHash =
|
const contractTermsHash =
|
||||||
cryptoComp.proposalIdToContractTermsHash[
|
cryptoComp.proposalIdToContractTermsHash[
|
||||||
backupPurchase.proposal_id
|
backupPurchase.proposal_id
|
||||||
];
|
];
|
||||||
let maxWireFee: AmountJson;
|
let maxWireFee: AmountJson;
|
||||||
if (parsedContractTerms.max_wire_fee) {
|
if (parsedContractTerms.max_wire_fee) {
|
||||||
@ -755,6 +755,7 @@ export async function importBackup(
|
|||||||
autoRefundDeadline: TalerProtocolTimestamp.never(),
|
autoRefundDeadline: TalerProtocolTimestamp.never(),
|
||||||
refundStatusRetryInfo: resetRetryInfo(),
|
refundStatusRetryInfo: resetRetryInfo(),
|
||||||
lastRefundStatusError: undefined,
|
lastRefundStatusError: undefined,
|
||||||
|
refundAwaiting: undefined,
|
||||||
timestampAccept: backupPurchase.timestamp_accept,
|
timestampAccept: backupPurchase.timestamp_accept,
|
||||||
timestampFirstSuccessfulPay:
|
timestampFirstSuccessfulPay:
|
||||||
backupPurchase.timestamp_first_successful_pay,
|
backupPurchase.timestamp_first_successful_pay,
|
||||||
|
@ -443,6 +443,7 @@ async function recordConfirmPay(
|
|||||||
refundQueryRequested: false,
|
refundQueryRequested: false,
|
||||||
timestampFirstSuccessfulPay: undefined,
|
timestampFirstSuccessfulPay: undefined,
|
||||||
autoRefundDeadline: undefined,
|
autoRefundDeadline: undefined,
|
||||||
|
refundAwaiting: undefined,
|
||||||
paymentSubmitPending: true,
|
paymentSubmitPending: true,
|
||||||
refunds: {},
|
refunds: {},
|
||||||
merchantPaySig: undefined,
|
merchantPaySig: undefined,
|
||||||
@ -987,18 +988,16 @@ async function storeFirstPaySuccess(
|
|||||||
purchase.lastSessionId = sessionId;
|
purchase.lastSessionId = sessionId;
|
||||||
purchase.payRetryInfo = resetRetryInfo();
|
purchase.payRetryInfo = resetRetryInfo();
|
||||||
purchase.merchantPaySig = paySig;
|
purchase.merchantPaySig = paySig;
|
||||||
if (isFirst) {
|
const protoAr = purchase.download.contractData.autoRefund;
|
||||||
const protoAr = purchase.download.contractData.autoRefund;
|
if (protoAr) {
|
||||||
if (protoAr) {
|
const ar = Duration.fromTalerProtocolDuration(protoAr);
|
||||||
const ar = Duration.fromTalerProtocolDuration(protoAr);
|
logger.info("auto_refund present");
|
||||||
logger.info("auto_refund present");
|
purchase.refundQueryRequested = true;
|
||||||
purchase.refundQueryRequested = true;
|
purchase.refundStatusRetryInfo = resetRetryInfo();
|
||||||
purchase.refundStatusRetryInfo = resetRetryInfo();
|
purchase.lastRefundStatusError = undefined;
|
||||||
purchase.lastRefundStatusError = undefined;
|
purchase.autoRefundDeadline = AbsoluteTime.toTimestamp(
|
||||||
purchase.autoRefundDeadline = AbsoluteTime.toTimestamp(
|
AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
|
||||||
AbsoluteTime.addDuration(AbsoluteTime.now(), ar),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await tx.purchases.put(purchase);
|
await tx.purchases.put(purchase);
|
||||||
});
|
});
|
||||||
|
@ -101,29 +101,19 @@ export async function prepareRefund(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const awaiting = await queryAndSaveAwaitingRefund(ws, purchase)
|
||||||
|
const summary = calculateRefundSummary(purchase)
|
||||||
const proposalId = purchase.proposalId;
|
const proposalId = purchase.proposalId;
|
||||||
const rfs = Object.values(purchase.refunds)
|
|
||||||
|
|
||||||
let applied = 0;
|
|
||||||
let failed = 0;
|
|
||||||
const total = rfs.length;
|
|
||||||
rfs.forEach((refund) => {
|
|
||||||
if (refund.type === RefundState.Failed) {
|
|
||||||
failed = failed + 1;
|
|
||||||
}
|
|
||||||
if (refund.type === RefundState.Applied) {
|
|
||||||
applied = applied + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { contractData: c } = purchase.download
|
const { contractData: c } = purchase.download
|
||||||
|
|
||||||
return {
|
return {
|
||||||
proposalId,
|
proposalId,
|
||||||
amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
|
effectivePaid: Amounts.stringify(summary.amountEffectivePaid),
|
||||||
applied,
|
gone: Amounts.stringify(summary.amountRefundGone),
|
||||||
failed,
|
granted: Amounts.stringify(summary.amountRefundGranted),
|
||||||
total,
|
pending: summary.pendingAtExchange,
|
||||||
|
awaiting: Amounts.stringify(awaiting),
|
||||||
info: {
|
info: {
|
||||||
contractTermsHash: c.contractTermsHash,
|
contractTermsHash: c.contractTermsHash,
|
||||||
merchant: c.merchant,
|
merchant: c.merchant,
|
||||||
@ -533,6 +523,44 @@ async function acceptRefunds(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function calculateRefundSummary(p: PurchaseRecord): RefundSummary {
|
||||||
|
let amountRefundGranted = Amounts.getZero(
|
||||||
|
p.download.contractData.amount.currency,
|
||||||
|
);
|
||||||
|
let amountRefundGone = Amounts.getZero(
|
||||||
|
p.download.contractData.amount.currency,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pendingAtExchange = false;
|
||||||
|
|
||||||
|
Object.keys(p.refunds).forEach((rk) => {
|
||||||
|
const refund = p.refunds[rk];
|
||||||
|
if (refund.type === RefundState.Pending) {
|
||||||
|
pendingAtExchange = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
refund.type === RefundState.Applied ||
|
||||||
|
refund.type === RefundState.Pending
|
||||||
|
) {
|
||||||
|
amountRefundGranted = Amounts.add(
|
||||||
|
amountRefundGranted,
|
||||||
|
Amounts.sub(
|
||||||
|
refund.refundAmount,
|
||||||
|
refund.refundFee,
|
||||||
|
refund.totalRefreshCostBound,
|
||||||
|
).amount,
|
||||||
|
).amount;
|
||||||
|
} else {
|
||||||
|
amountRefundGone = Amounts.add(
|
||||||
|
amountRefundGone,
|
||||||
|
refund.refundAmount,
|
||||||
|
).amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { amountEffectivePaid: p.totalPayCost, amountRefundGone, amountRefundGranted, pendingAtExchange }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Summary of the refund status of a purchase.
|
* Summary of the refund status of a purchase.
|
||||||
*/
|
*/
|
||||||
@ -618,49 +646,15 @@ export async function applyRefund(
|
|||||||
throw Error("purchase no longer exists");
|
throw Error("purchase no longer exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = purchase;
|
const summary = calculateRefundSummary(purchase)
|
||||||
|
|
||||||
let amountRefundGranted = Amounts.getZero(
|
|
||||||
purchase.download.contractData.amount.currency,
|
|
||||||
);
|
|
||||||
let amountRefundGone = Amounts.getZero(
|
|
||||||
purchase.download.contractData.amount.currency,
|
|
||||||
);
|
|
||||||
|
|
||||||
let pendingAtExchange = false;
|
|
||||||
|
|
||||||
Object.keys(purchase.refunds).forEach((rk) => {
|
|
||||||
const refund = p.refunds[rk];
|
|
||||||
if (refund.type === RefundState.Pending) {
|
|
||||||
pendingAtExchange = true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
refund.type === RefundState.Applied ||
|
|
||||||
refund.type === RefundState.Pending
|
|
||||||
) {
|
|
||||||
amountRefundGranted = Amounts.add(
|
|
||||||
amountRefundGranted,
|
|
||||||
Amounts.sub(
|
|
||||||
refund.refundAmount,
|
|
||||||
refund.refundFee,
|
|
||||||
refund.totalRefreshCostBound,
|
|
||||||
).amount,
|
|
||||||
).amount;
|
|
||||||
} else {
|
|
||||||
amountRefundGone = Amounts.add(
|
|
||||||
amountRefundGone,
|
|
||||||
refund.refundAmount,
|
|
||||||
).amount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
proposalId: purchase.proposalId,
|
proposalId: purchase.proposalId,
|
||||||
amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
|
amountEffectivePaid: Amounts.stringify(summary.amountEffectivePaid),
|
||||||
amountRefundGone: Amounts.stringify(amountRefundGone),
|
amountRefundGone: Amounts.stringify(summary.amountRefundGone),
|
||||||
amountRefundGranted: Amounts.stringify(amountRefundGranted),
|
amountRefundGranted: Amounts.stringify(summary.amountRefundGranted),
|
||||||
pendingAtExchange,
|
pendingAtExchange: summary.pendingAtExchange,
|
||||||
info: {
|
info: {
|
||||||
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
merchant: purchase.download.contractData.merchant,
|
merchant: purchase.download.contractData.merchant,
|
||||||
@ -691,6 +685,59 @@ export async function processPurchaseQueryRefund(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function queryAndSaveAwaitingRefund(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
purchase: PurchaseRecord,
|
||||||
|
waitForAutoRefund?: boolean): Promise<AmountJson> {
|
||||||
|
const requestUrl = new URL(
|
||||||
|
`orders/${purchase.download.contractData.orderId}`,
|
||||||
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
|
);
|
||||||
|
requestUrl.searchParams.set(
|
||||||
|
"h_contract",
|
||||||
|
purchase.download.contractData.contractTermsHash,
|
||||||
|
);
|
||||||
|
// Long-poll for one second
|
||||||
|
if (waitForAutoRefund) {
|
||||||
|
requestUrl.searchParams.set("timeout_ms", "1000");
|
||||||
|
requestUrl.searchParams.set("await_refund_obtained", "yes");
|
||||||
|
logger.trace("making long-polling request for auto-refund");
|
||||||
|
}
|
||||||
|
const resp = await ws.http.get(requestUrl.href);
|
||||||
|
const orderStatus = await readSuccessResponseJsonOrThrow(
|
||||||
|
resp,
|
||||||
|
codecForMerchantOrderStatusPaid(),
|
||||||
|
);
|
||||||
|
if (!orderStatus.refunded) {
|
||||||
|
// Wait for retry ...
|
||||||
|
return Amounts.getZero(purchase.totalPayCost.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
const refundAwaiting = Amounts.sub(
|
||||||
|
Amounts.parseOrThrow(orderStatus.refund_amount),
|
||||||
|
Amounts.parseOrThrow(orderStatus.refund_taken)
|
||||||
|
).amount
|
||||||
|
|
||||||
|
console.log("refund waiting found, ", refundAwaiting, orderStatus, purchase.refundAwaiting, purchase.refundAwaiting && Amounts.cmp(refundAwaiting, purchase.refundAwaiting))
|
||||||
|
|
||||||
|
if (purchase.refundAwaiting === undefined || Amounts.cmp(refundAwaiting, purchase.refundAwaiting) !== 0) {
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({ purchases: x.purchases }))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const p = await tx.purchases.get(purchase.proposalId);
|
||||||
|
if (!p) {
|
||||||
|
logger.warn("purchase does not exist anymore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p.refundAwaiting = refundAwaiting
|
||||||
|
await tx.purchases.put(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return refundAwaiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function processPurchaseQueryRefundImpl(
|
async function processPurchaseQueryRefundImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
@ -719,33 +766,13 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
|
|
||||||
if (purchase.timestampFirstSuccessfulPay) {
|
if (purchase.timestampFirstSuccessfulPay) {
|
||||||
if (
|
if (
|
||||||
waitForAutoRefund &&
|
!purchase.autoRefundDeadline ||
|
||||||
purchase.autoRefundDeadline &&
|
|
||||||
!AbsoluteTime.isExpired(
|
!AbsoluteTime.isExpired(
|
||||||
AbsoluteTime.fromTimestamp(purchase.autoRefundDeadline),
|
AbsoluteTime.fromTimestamp(purchase.autoRefundDeadline),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const requestUrl = new URL(
|
const awaitingAmount = await queryAndSaveAwaitingRefund(ws, purchase, waitForAutoRefund)
|
||||||
`orders/${purchase.download.contractData.orderId}`,
|
if (Amounts.isZero(awaitingAmount)) return;
|
||||||
purchase.download.contractData.merchantBaseUrl,
|
|
||||||
);
|
|
||||||
requestUrl.searchParams.set(
|
|
||||||
"h_contract",
|
|
||||||
purchase.download.contractData.contractTermsHash,
|
|
||||||
);
|
|
||||||
// Long-poll for one second
|
|
||||||
requestUrl.searchParams.set("timeout_ms", "1000");
|
|
||||||
requestUrl.searchParams.set("await_refund_obtained", "yes");
|
|
||||||
logger.trace("making long-polling request for auto-refund");
|
|
||||||
const resp = await ws.http.get(requestUrl.href);
|
|
||||||
const orderStatus = await readSuccessResponseJsonOrThrow(
|
|
||||||
resp,
|
|
||||||
codecForMerchantOrderStatusPaid(),
|
|
||||||
);
|
|
||||||
if (!orderStatus.refunded) {
|
|
||||||
// Wait for retry ...
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestUrl = new URL(
|
const requestUrl = new URL(
|
||||||
|
@ -49,6 +49,16 @@ import { processWithdrawGroup } from "./withdraw.js";
|
|||||||
|
|
||||||
const logger = new Logger("taler-wallet-core:transactions.ts");
|
const logger = new Logger("taler-wallet-core:transactions.ts");
|
||||||
|
|
||||||
|
export enum TombstoneTag {
|
||||||
|
DeleteWithdrawalGroup = "delete-withdrawal-group",
|
||||||
|
DeleteReserve = "delete-reserve",
|
||||||
|
DeletePayment = "delete-payment",
|
||||||
|
DeleteTip = "delete-tip",
|
||||||
|
DeleteRefreshGroup = "delete-refresh-group",
|
||||||
|
DeleteDepositGroup = "delete-deposit-group",
|
||||||
|
DeleteRefund = "delete-refund",
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an event ID from the type and the primary key for the event.
|
* Create an event ID from the type and the primary key for the event.
|
||||||
*/
|
*/
|
||||||
@ -286,25 +296,6 @@ export async function getTransactions(
|
|||||||
TransactionType.Payment,
|
TransactionType.Payment,
|
||||||
pr.proposalId,
|
pr.proposalId,
|
||||||
);
|
);
|
||||||
const err = pr.lastPayError ?? pr.lastRefundStatusError;
|
|
||||||
transactions.push({
|
|
||||||
type: TransactionType.Payment,
|
|
||||||
amountRaw: Amounts.stringify(contractData.amount),
|
|
||||||
amountEffective: Amounts.stringify(pr.totalPayCost),
|
|
||||||
status: pr.timestampFirstSuccessfulPay
|
|
||||||
? PaymentStatus.Paid
|
|
||||||
: PaymentStatus.Accepted,
|
|
||||||
pending:
|
|
||||||
!pr.timestampFirstSuccessfulPay &&
|
|
||||||
pr.abortStatus === AbortStatus.None,
|
|
||||||
timestamp: pr.timestampAccept,
|
|
||||||
transactionId: paymentTransactionId,
|
|
||||||
proposalId: pr.proposalId,
|
|
||||||
info: info,
|
|
||||||
frozen: pr.payFrozen ?? false,
|
|
||||||
...(err ? { error: err } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const refundGroupKeys = new Set<string>();
|
const refundGroupKeys = new Set<string>();
|
||||||
|
|
||||||
for (const rk of Object.keys(pr.refunds)) {
|
for (const rk of Object.keys(pr.refunds)) {
|
||||||
@ -313,6 +304,9 @@ export async function getTransactions(
|
|||||||
refundGroupKeys.add(groupKey);
|
refundGroupKeys.add(groupKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let totalRefundRaw = Amounts.getZero(contractData.amount.currency);
|
||||||
|
let totalRefundEffective = Amounts.getZero(contractData.amount.currency);
|
||||||
|
|
||||||
for (const groupKey of refundGroupKeys.values()) {
|
for (const groupKey of refundGroupKeys.values()) {
|
||||||
const refundTombstoneId = makeEventId(
|
const refundTombstoneId = makeEventId(
|
||||||
TombstoneTag.DeleteRefund,
|
TombstoneTag.DeleteRefund,
|
||||||
@ -356,6 +350,10 @@ export async function getTransactions(
|
|||||||
if (!r0) {
|
if (!r0) {
|
||||||
throw Error("invariant violated");
|
throw Error("invariant violated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount;
|
||||||
|
totalRefundEffective = Amounts.add(totalRefundEffective, amountEffective).amount;
|
||||||
|
|
||||||
transactions.push({
|
transactions.push({
|
||||||
type: TransactionType.Refund,
|
type: TransactionType.Refund,
|
||||||
info,
|
info,
|
||||||
@ -364,10 +362,34 @@ export async function getTransactions(
|
|||||||
timestamp: r0.obtainedTime,
|
timestamp: r0.obtainedTime,
|
||||||
amountEffective: Amounts.stringify(amountEffective),
|
amountEffective: Amounts.stringify(amountEffective),
|
||||||
amountRaw: Amounts.stringify(amountRaw),
|
amountRaw: Amounts.stringify(amountRaw),
|
||||||
|
refundPending: pr.refundAwaiting === undefined ? undefined : Amounts.stringify(pr.refundAwaiting),
|
||||||
pending: false,
|
pending: false,
|
||||||
frozen: false,
|
frozen: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const err = pr.lastPayError ?? pr.lastRefundStatusError;
|
||||||
|
transactions.push({
|
||||||
|
type: TransactionType.Payment,
|
||||||
|
amountRaw: Amounts.stringify(contractData.amount),
|
||||||
|
amountEffective: Amounts.stringify(pr.totalPayCost),
|
||||||
|
totalRefundRaw: Amounts.stringify(totalRefundRaw),
|
||||||
|
totalRefundEffective: Amounts.stringify(totalRefundEffective),
|
||||||
|
refundPending: pr.refundAwaiting === undefined ? undefined : Amounts.stringify(pr.refundAwaiting),
|
||||||
|
status: pr.timestampFirstSuccessfulPay
|
||||||
|
? PaymentStatus.Paid
|
||||||
|
: PaymentStatus.Accepted,
|
||||||
|
pending:
|
||||||
|
!pr.timestampFirstSuccessfulPay &&
|
||||||
|
pr.abortStatus === AbortStatus.None,
|
||||||
|
timestamp: pr.timestampAccept,
|
||||||
|
transactionId: paymentTransactionId,
|
||||||
|
proposalId: pr.proposalId,
|
||||||
|
info: info,
|
||||||
|
frozen: pr.payFrozen ?? false,
|
||||||
|
...(err ? { error: err } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tx.tips.iter().forEachAsync(async (tipRecord) => {
|
tx.tips.iter().forEachAsync(async (tipRecord) => {
|
||||||
@ -419,16 +441,6 @@ export async function getTransactions(
|
|||||||
return { transactions: [...txNotPending, ...txPending] };
|
return { transactions: [...txNotPending, ...txPending] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TombstoneTag {
|
|
||||||
DeleteWithdrawalGroup = "delete-withdrawal-group",
|
|
||||||
DeleteReserve = "delete-reserve",
|
|
||||||
DeletePayment = "delete-payment",
|
|
||||||
DeleteTip = "delete-tip",
|
|
||||||
DeleteRefreshGroup = "delete-refresh-group",
|
|
||||||
DeleteDepositGroup = "delete-deposit-group",
|
|
||||||
DeleteRefund = "delete-refund",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immediately retry the underlying operation
|
* Immediately retry the underlying operation
|
||||||
* of a transaction.
|
* of a transaction.
|
||||||
@ -442,28 +454,33 @@ export async function retryTransaction(
|
|||||||
const [type, ...rest] = transactionId.split(":");
|
const [type, ...rest] = transactionId.split(":");
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TransactionType.Deposit:
|
case TransactionType.Deposit: {
|
||||||
const depositGroupId = rest[0];
|
const depositGroupId = rest[0];
|
||||||
processDepositGroup(ws, depositGroupId, {
|
processDepositGroup(ws, depositGroupId, {
|
||||||
forceNow: true,
|
forceNow: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case TransactionType.Withdrawal:
|
}
|
||||||
|
case TransactionType.Withdrawal: {
|
||||||
const withdrawalGroupId = rest[0];
|
const withdrawalGroupId = rest[0];
|
||||||
await processWithdrawGroup(ws, withdrawalGroupId, { forceNow: true });
|
await processWithdrawGroup(ws, withdrawalGroupId, { forceNow: true });
|
||||||
break;
|
break;
|
||||||
case TransactionType.Payment:
|
}
|
||||||
|
case TransactionType.Payment: {
|
||||||
const proposalId = rest[0];
|
const proposalId = rest[0];
|
||||||
await processPurchasePay(ws, proposalId, { forceNow: true });
|
await processPurchasePay(ws, proposalId, { forceNow: true });
|
||||||
break;
|
break;
|
||||||
case TransactionType.Tip:
|
}
|
||||||
|
case TransactionType.Tip: {
|
||||||
const walletTipId = rest[0];
|
const walletTipId = rest[0];
|
||||||
await processTip(ws, walletTipId, { forceNow: true });
|
await processTip(ws, walletTipId, { forceNow: true });
|
||||||
break;
|
break;
|
||||||
case TransactionType.Refresh:
|
}
|
||||||
|
case TransactionType.Refresh: {
|
||||||
const refreshGroupId = rest[0];
|
const refreshGroupId = rest[0];
|
||||||
await processRefreshGroup(ws, refreshGroupId, { forceNow: true });
|
await processRefreshGroup(ws, refreshGroupId, { forceNow: true });
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1235,10 +1235,10 @@ class InternalWalletStateImpl implements InternalWalletState {
|
|||||||
const key = `${exchangeBaseUrl}:${denomPubHash}`;
|
const key = `${exchangeBaseUrl}:${denomPubHash}`;
|
||||||
const cached = this.denomCache[key];
|
const cached = this.denomCache[key];
|
||||||
if (cached) {
|
if (cached) {
|
||||||
logger.info("using cached denom");
|
logger.trace("using cached denom");
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
logger.info("looking up denom denom");
|
logger.trace("looking up denom denom");
|
||||||
const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
|
const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]);
|
||||||
if (d) {
|
if (d) {
|
||||||
this.denomCache[key] = d;
|
this.denomCache[key] = d;
|
||||||
|
2
packages/taler-wallet-webextension/compile_core.sh
Executable file
2
packages/taler-wallet-webextension/compile_core.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
pnpm run --filter @gnu-taler/taler-wallet-core compile
|
3
packages/taler-wallet-webextension/compile_util.sh
Executable file
3
packages/taler-wallet-webextension/compile_util.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
pnpm run --filter @gnu-taler/taler-util compile
|
||||||
|
|
@ -33,6 +33,7 @@ export const Complete = createExample(TestedComponent, {
|
|||||||
state: {
|
state: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
amount: Amounts.parseOrThrow("USD:1"),
|
amount: Amounts.parseOrThrow("USD:1"),
|
||||||
|
granted: Amounts.parseOrThrow("USD:1"),
|
||||||
hook: undefined,
|
hook: undefined,
|
||||||
merchantName: "the merchant",
|
merchantName: "the merchant",
|
||||||
products: undefined,
|
products: undefined,
|
||||||
@ -44,9 +45,10 @@ export const InProgress = createExample(TestedComponent, {
|
|||||||
status: "in-progress",
|
status: "in-progress",
|
||||||
hook: undefined,
|
hook: undefined,
|
||||||
amount: Amounts.parseOrThrow("USD:1"),
|
amount: Amounts.parseOrThrow("USD:1"),
|
||||||
|
awaitingAmount: Amounts.parseOrThrow("USD:1"),
|
||||||
|
granted: Amounts.parseOrThrow("USD:0"),
|
||||||
merchantName: "the merchant",
|
merchantName: "the merchant",
|
||||||
products: undefined,
|
products: undefined,
|
||||||
progress: 0.5,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -58,6 +60,8 @@ export const Ready = createExample(TestedComponent, {
|
|||||||
ignore: {},
|
ignore: {},
|
||||||
|
|
||||||
amount: Amounts.parseOrThrow("USD:1"),
|
amount: Amounts.parseOrThrow("USD:1"),
|
||||||
|
awaitingAmount: Amounts.parseOrThrow("USD:1"),
|
||||||
|
granted: Amounts.parseOrThrow("USD:0"),
|
||||||
merchantName: "the merchant",
|
merchantName: "the merchant",
|
||||||
products: [],
|
products: [],
|
||||||
orderId: "abcdef",
|
orderId: "abcdef",
|
||||||
@ -73,6 +77,8 @@ export const WithAProductList = createExample(TestedComponent, {
|
|||||||
accept: {},
|
accept: {},
|
||||||
ignore: {},
|
ignore: {},
|
||||||
amount: Amounts.parseOrThrow("USD:1"),
|
amount: Amounts.parseOrThrow("USD:1"),
|
||||||
|
awaitingAmount: Amounts.parseOrThrow("USD:1"),
|
||||||
|
granted: Amounts.parseOrThrow("USD:0"),
|
||||||
merchantName: "the merchant",
|
merchantName: "the merchant",
|
||||||
products: [
|
products: [
|
||||||
{
|
{
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Amounts, NotificationType, PrepareRefundResult } from "@gnu-taler/taler-util";
|
import { AmountJson, Amounts, NotificationType, PrepareRefundResult } from "@gnu-taler/taler-util";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { mountHook } from "../test-utils.js";
|
import { mountHook } from "../test-utils.js";
|
||||||
import { SubsHandler } from "./Pay.test.js";
|
import { SubsHandler } from "./Pay.test.js";
|
||||||
@ -62,10 +62,12 @@ describe("Refund CTA states", () => {
|
|||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
useComponentState("taler://refund/asdasdas", {
|
useComponentState("taler://refund/asdasdas", {
|
||||||
prepareRefund: async () => ({
|
prepareRefund: async () => ({
|
||||||
total: 0,
|
effectivePaid: 'EUR:2',
|
||||||
applied: 0,
|
awaiting: 'EUR:2',
|
||||||
failed: 0,
|
gone: 'EUR:0',
|
||||||
amountEffectivePaid: 'EUR:2',
|
granted: 'EUR:0',
|
||||||
|
pending: false,
|
||||||
|
proposalId: '1',
|
||||||
info: {
|
info: {
|
||||||
contractTermsHash: '123',
|
contractTermsHash: '123',
|
||||||
merchant: {
|
merchant: {
|
||||||
@ -107,10 +109,12 @@ describe("Refund CTA states", () => {
|
|||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
useComponentState("taler://refund/asdasdas", {
|
useComponentState("taler://refund/asdasdas", {
|
||||||
prepareRefund: async () => ({
|
prepareRefund: async () => ({
|
||||||
total: 0,
|
effectivePaid: 'EUR:2',
|
||||||
applied: 0,
|
awaiting: 'EUR:2',
|
||||||
failed: 0,
|
gone: 'EUR:0',
|
||||||
amountEffectivePaid: 'EUR:2',
|
granted: 'EUR:0',
|
||||||
|
pending: false,
|
||||||
|
proposalId: '1',
|
||||||
info: {
|
info: {
|
||||||
contractTermsHash: '123',
|
contractTermsHash: '123',
|
||||||
merchant: {
|
merchant: {
|
||||||
@ -161,21 +165,30 @@ describe("Refund CTA states", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should be in progress when doing refresh", async () => {
|
it("should be in progress when doing refresh", async () => {
|
||||||
let numApplied = 1;
|
let granted = Amounts.getZero('EUR')
|
||||||
|
const unit: AmountJson = { currency: 'EUR', value: 1, fraction: 0 }
|
||||||
|
const refunded: AmountJson = { currency: 'EUR', value: 2, fraction: 0 }
|
||||||
|
let awaiting: AmountJson = refunded
|
||||||
|
let pending = true;
|
||||||
|
|
||||||
const subscriptions = new SubsHandler();
|
const subscriptions = new SubsHandler();
|
||||||
|
|
||||||
function notifyMelt(): void {
|
function notifyMelt(): void {
|
||||||
numApplied++;
|
granted = Amounts.add(granted, unit).amount;
|
||||||
|
pending = granted.value < refunded.value;
|
||||||
|
awaiting = Amounts.sub(refunded, granted).amount;
|
||||||
subscriptions.notifyEvent(NotificationType.RefreshMelted)
|
subscriptions.notifyEvent(NotificationType.RefreshMelted)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
|
||||||
useComponentState("taler://refund/asdasdas", {
|
useComponentState("taler://refund/asdasdas", {
|
||||||
prepareRefund: async () => ({
|
prepareRefund: async () => ({
|
||||||
total: 3,
|
awaiting: Amounts.stringify(awaiting),
|
||||||
applied: numApplied,
|
effectivePaid: 'EUR:2',
|
||||||
failed: 0,
|
gone: 'EUR:0',
|
||||||
amountEffectivePaid: 'EUR:2',
|
granted: Amounts.stringify(granted),
|
||||||
|
pending,
|
||||||
|
proposalId: '1',
|
||||||
info: {
|
info: {
|
||||||
contractTermsHash: '123',
|
contractTermsHash: '123',
|
||||||
merchant: {
|
merchant: {
|
||||||
@ -201,12 +214,12 @@ describe("Refund CTA states", () => {
|
|||||||
{
|
{
|
||||||
const state = getLastResultOrThrow()
|
const state = getLastResultOrThrow()
|
||||||
|
|
||||||
if (state.status !== 'in-progress') expect.fail();
|
if (state.status !== 'in-progress') expect.fail('1');
|
||||||
if (state.hook) expect.fail();
|
if (state.hook) expect.fail();
|
||||||
expect(state.merchantName).eq('the merchant name');
|
expect(state.merchantName).eq('the merchant name');
|
||||||
expect(state.products).undefined;
|
expect(state.products).undefined;
|
||||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
|
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
|
||||||
expect(state.progress).closeTo(1 / 3, 0.01)
|
// expect(state.progress).closeTo(1 / 3, 0.01)
|
||||||
|
|
||||||
notifyMelt()
|
notifyMelt()
|
||||||
}
|
}
|
||||||
@ -216,12 +229,12 @@ describe("Refund CTA states", () => {
|
|||||||
{
|
{
|
||||||
const state = getLastResultOrThrow()
|
const state = getLastResultOrThrow()
|
||||||
|
|
||||||
if (state.status !== 'in-progress') expect.fail();
|
if (state.status !== 'in-progress') expect.fail('2');
|
||||||
if (state.hook) expect.fail();
|
if (state.hook) expect.fail();
|
||||||
expect(state.merchantName).eq('the merchant name');
|
expect(state.merchantName).eq('the merchant name');
|
||||||
expect(state.products).undefined;
|
expect(state.products).undefined;
|
||||||
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
|
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"))
|
||||||
expect(state.progress).closeTo(2 / 3, 0.01)
|
// expect(state.progress).closeTo(2 / 3, 0.01)
|
||||||
|
|
||||||
notifyMelt()
|
notifyMelt()
|
||||||
}
|
}
|
||||||
@ -231,7 +244,7 @@ describe("Refund CTA states", () => {
|
|||||||
{
|
{
|
||||||
const state = getLastResultOrThrow()
|
const state = getLastResultOrThrow()
|
||||||
|
|
||||||
if (state.status !== 'completed') expect.fail();
|
if (state.status !== 'completed') expect.fail('3');
|
||||||
if (state.hook) expect.fail();
|
if (state.hook) expect.fail();
|
||||||
expect(state.merchantName).eq('the merchant name');
|
expect(state.merchantName).eq('the merchant name');
|
||||||
expect(state.products).undefined;
|
expect(state.products).undefined;
|
||||||
|
@ -34,7 +34,6 @@ import { LoadingError } from "../components/LoadingError.js";
|
|||||||
import { LogoHeader } from "../components/LogoHeader.js";
|
import { LogoHeader } from "../components/LogoHeader.js";
|
||||||
import { Part } from "../components/Part.js";
|
import { Part } from "../components/Part.js";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
ButtonSuccess,
|
ButtonSuccess,
|
||||||
SubTitle,
|
SubTitle,
|
||||||
WalletAction,
|
WalletAction,
|
||||||
@ -99,6 +98,12 @@ export function View({ state }: ViewProps): VNode {
|
|||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title={<i18n.Translate>Total to refund</i18n.Translate>}
|
title={<i18n.Translate>Total to refund</i18n.Translate>}
|
||||||
|
text={<Amount value={state.awaitingAmount} />}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
<Part
|
||||||
|
big
|
||||||
|
title={<i18n.Translate>Refunded</i18n.Translate>}
|
||||||
text={<Amount value={state.amount} />}
|
text={<Amount value={state.amount} />}
|
||||||
kind="negative"
|
kind="negative"
|
||||||
/>
|
/>
|
||||||
@ -108,9 +113,9 @@ export function View({ state }: ViewProps): VNode {
|
|||||||
<ProductList products={state.products} />
|
<ProductList products={state.products} />
|
||||||
</section>
|
</section>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<section>
|
{/* <section>
|
||||||
<ProgressBar value={state.progress} />
|
<ProgressBar value={state.progress} />
|
||||||
</section>
|
</section> */}
|
||||||
</WalletAction>
|
</WalletAction>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -128,6 +133,14 @@ export function View({ state }: ViewProps): VNode {
|
|||||||
<i18n.Translate>this refund is already accepted.</i18n.Translate>
|
<i18n.Translate>this refund is already accepted.</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<Part
|
||||||
|
big
|
||||||
|
title={<i18n.Translate>Total to refunded</i18n.Translate>}
|
||||||
|
text={<Amount value={state.granted} />}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
</WalletAction>
|
</WalletAction>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -150,9 +163,23 @@ export function View({ state }: ViewProps): VNode {
|
|||||||
<section>
|
<section>
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title={<i18n.Translate>Total to refund</i18n.Translate>}
|
title={<i18n.Translate>Order amount</i18n.Translate>}
|
||||||
text={<Amount value={state.amount} />}
|
text={<Amount value={state.amount} />}
|
||||||
kind="negative"
|
kind="neutral"
|
||||||
|
/>
|
||||||
|
{Amounts.isNonZero(state.granted) && (
|
||||||
|
<Part
|
||||||
|
big
|
||||||
|
title={<i18n.Translate>Already refunded</i18n.Translate>}
|
||||||
|
text={<Amount value={state.granted} />}
|
||||||
|
kind="neutral"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Part
|
||||||
|
big
|
||||||
|
title={<i18n.Translate>Refund offered</i18n.Translate>}
|
||||||
|
text={<Amount value={state.awaitingAmount} />}
|
||||||
|
kind="positive"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{state.products && state.products.length ? (
|
{state.products && state.products.length ? (
|
||||||
@ -164,9 +191,6 @@ export function View({ state }: ViewProps): VNode {
|
|||||||
<ButtonSuccess onClick={state.accept.onClick}>
|
<ButtonSuccess onClick={state.accept.onClick}>
|
||||||
<i18n.Translate>Confirm refund</i18n.Translate>
|
<i18n.Translate>Confirm refund</i18n.Translate>
|
||||||
</ButtonSuccess>
|
</ButtonSuccess>
|
||||||
<Button onClick={state.ignore.onClick}>
|
|
||||||
<i18n.Translate>Ignore</i18n.Translate>
|
|
||||||
</Button>
|
|
||||||
</section>
|
</section>
|
||||||
</WalletAction>
|
</WalletAction>
|
||||||
);
|
);
|
||||||
@ -184,6 +208,8 @@ interface Ready {
|
|||||||
merchantName: string;
|
merchantName: string;
|
||||||
products: Product[] | undefined;
|
products: Product[] | undefined;
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
awaitingAmount: AmountJson;
|
||||||
|
granted: AmountJson;
|
||||||
accept: ButtonHandler;
|
accept: ButtonHandler;
|
||||||
ignore: ButtonHandler;
|
ignore: ButtonHandler;
|
||||||
orderId: string;
|
orderId: string;
|
||||||
@ -199,7 +225,8 @@ interface InProgress {
|
|||||||
merchantName: string;
|
merchantName: string;
|
||||||
products: Product[] | undefined;
|
products: Product[] | undefined;
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
progress: number;
|
awaitingAmount: AmountJson;
|
||||||
|
granted: AmountJson;
|
||||||
}
|
}
|
||||||
interface Completed {
|
interface Completed {
|
||||||
status: "completed";
|
status: "completed";
|
||||||
@ -207,6 +234,7 @@ interface Completed {
|
|||||||
merchantName: string;
|
merchantName: string;
|
||||||
products: Product[] | undefined;
|
products: Product[] | undefined;
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
granted: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useComponentState(
|
export function useComponentState(
|
||||||
@ -253,25 +281,27 @@ export function useComponentState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = refund.total > refund.applied + refund.failed;
|
const awaitingAmount = Amounts.parseOrThrow(refund.awaiting);
|
||||||
const completed = refund.total > 0 && refund.applied === refund.total;
|
|
||||||
|
|
||||||
if (pending) {
|
if (Amounts.isZero(awaitingAmount)) {
|
||||||
return {
|
|
||||||
status: "in-progress",
|
|
||||||
hook: undefined,
|
|
||||||
amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
|
|
||||||
merchantName: info.response.refund.info.merchant.name,
|
|
||||||
products: info.response.refund.info.products,
|
|
||||||
progress: (refund.applied + refund.failed) / refund.total,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completed) {
|
|
||||||
return {
|
return {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
hook: undefined,
|
hook: undefined,
|
||||||
amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
|
amount: Amounts.parseOrThrow(info.response.refund.effectivePaid),
|
||||||
|
granted: Amounts.parseOrThrow(info.response.refund.granted),
|
||||||
|
merchantName: info.response.refund.info.merchant.name,
|
||||||
|
products: info.response.refund.info.products,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refund.pending) {
|
||||||
|
return {
|
||||||
|
status: "in-progress",
|
||||||
|
hook: undefined,
|
||||||
|
awaitingAmount,
|
||||||
|
amount: Amounts.parseOrThrow(info.response.refund.effectivePaid),
|
||||||
|
granted: Amounts.parseOrThrow(info.response.refund.granted),
|
||||||
|
|
||||||
merchantName: info.response.refund.info.merchant.name,
|
merchantName: info.response.refund.info.merchant.name,
|
||||||
products: info.response.refund.info.products,
|
products: info.response.refund.info.products,
|
||||||
};
|
};
|
||||||
@ -280,7 +310,9 @@ export function useComponentState(
|
|||||||
return {
|
return {
|
||||||
status: "ready",
|
status: "ready",
|
||||||
hook: undefined,
|
hook: undefined,
|
||||||
amount: Amounts.parseOrThrow(info.response.refund.amountEffectivePaid),
|
amount: Amounts.parseOrThrow(info.response.refund.effectivePaid),
|
||||||
|
granted: Amounts.parseOrThrow(info.response.refund.granted),
|
||||||
|
awaitingAmount,
|
||||||
merchantName: info.response.refund.info.merchant.name,
|
merchantName: info.response.refund.info.merchant.name,
|
||||||
products: info.response.refund.info.products,
|
products: info.response.refund.info.products,
|
||||||
orderId: info.response.refund.info.orderId,
|
orderId: info.response.refund.info.orderId,
|
||||||
|
@ -78,6 +78,9 @@ const exampleData = {
|
|||||||
summary: "the summary",
|
summary: "the summary",
|
||||||
fulfillmentMessage: "",
|
fulfillmentMessage: "",
|
||||||
},
|
},
|
||||||
|
refundPending: undefined,
|
||||||
|
totalRefundEffective: "USD:0",
|
||||||
|
totalRefundRaw: "USD:0",
|
||||||
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
|
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
|
||||||
status: PaymentStatus.Accepted,
|
status: PaymentStatus.Accepted,
|
||||||
} as TransactionPayment,
|
} as TransactionPayment,
|
||||||
@ -112,6 +115,7 @@ const exampleData = {
|
|||||||
summary: "the summary",
|
summary: "the summary",
|
||||||
fulfillmentMessage: "",
|
fulfillmentMessage: "",
|
||||||
},
|
},
|
||||||
|
refundPending: undefined,
|
||||||
} as TransactionRefund,
|
} as TransactionRefund,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,6 +83,9 @@ const exampleData = {
|
|||||||
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
|
summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth",
|
||||||
fulfillmentMessage: "",
|
fulfillmentMessage: "",
|
||||||
},
|
},
|
||||||
|
refundPending: undefined,
|
||||||
|
totalRefundEffective: "USD:0",
|
||||||
|
totalRefundRaw: "USD:0",
|
||||||
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
|
proposalId: "1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0",
|
||||||
status: PaymentStatus.Accepted,
|
status: PaymentStatus.Accepted,
|
||||||
} as TransactionPayment,
|
} as TransactionPayment,
|
||||||
@ -117,6 +120,7 @@ const exampleData = {
|
|||||||
summary: "the summary",
|
summary: "the summary",
|
||||||
fulfillmentMessage: "",
|
fulfillmentMessage: "",
|
||||||
},
|
},
|
||||||
|
refundPending: undefined,
|
||||||
} as TransactionRefund,
|
} as TransactionRefund,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,7 +40,6 @@ import {
|
|||||||
ButtonPrimary,
|
ButtonPrimary,
|
||||||
CenteredDialog,
|
CenteredDialog,
|
||||||
InfoBox,
|
InfoBox,
|
||||||
LargeText,
|
|
||||||
ListOfProducts,
|
ListOfProducts,
|
||||||
Overlay,
|
Overlay,
|
||||||
RowBorderGray,
|
RowBorderGray,
|
||||||
@ -51,6 +50,7 @@ import {
|
|||||||
import { Time } from "../components/Time.js";
|
import { Time } from "../components/Time.js";
|
||||||
import { useTranslationContext } from "../context/translation.js";
|
import { useTranslationContext } from "../context/translation.js";
|
||||||
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
|
||||||
|
import { Pages } from "../NavigationBar.js";
|
||||||
import * as wxApi from "../wxApi.js";
|
import * as wxApi from "../wxApi.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -344,6 +344,17 @@ export function TransactionView({
|
|||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
).amount;
|
).amount;
|
||||||
|
|
||||||
|
const refundFee = Amounts.sub(
|
||||||
|
Amounts.parseOrThrow(transaction.totalRefundRaw),
|
||||||
|
Amounts.parseOrThrow(transaction.totalRefundEffective),
|
||||||
|
).amount;
|
||||||
|
const refunded = Amounts.isNonZero(
|
||||||
|
Amounts.parseOrThrow(transaction.totalRefundRaw),
|
||||||
|
);
|
||||||
|
const pendingRefund =
|
||||||
|
transaction.refundPending === undefined
|
||||||
|
? undefined
|
||||||
|
: Amounts.parseOrThrow(transaction.refundPending);
|
||||||
return (
|
return (
|
||||||
<TransactionTemplate>
|
<TransactionTemplate>
|
||||||
<SubTitle>
|
<SubTitle>
|
||||||
@ -360,18 +371,54 @@ export function TransactionView({
|
|||||||
text={<Amount value={transaction.amountEffective} />}
|
text={<Amount value={transaction.amountEffective} />}
|
||||||
kind="negative"
|
kind="negative"
|
||||||
/>
|
/>
|
||||||
<Part
|
{Amounts.isNonZero(fee) && (
|
||||||
big
|
<Fragment>
|
||||||
title={<i18n.Translate>Purchase amount</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={transaction.amountRaw} />}
|
big
|
||||||
kind="neutral"
|
title={<i18n.Translate>Purchase amount</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={transaction.amountRaw} />}
|
||||||
<Part
|
kind="neutral"
|
||||||
big
|
/>
|
||||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={fee} />}
|
title={<i18n.Translate>Purchase Fee</i18n.Translate>}
|
||||||
kind="negative"
|
text={<Amount value={fee} />}
|
||||||
/>
|
kind="negative"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
{refunded && (
|
||||||
|
<Fragment>
|
||||||
|
<Part
|
||||||
|
big
|
||||||
|
title={<i18n.Translate>Total refunded</i18n.Translate>}
|
||||||
|
text={<Amount value={transaction.totalRefundEffective} />}
|
||||||
|
kind="positive"
|
||||||
|
/>
|
||||||
|
{Amounts.isNonZero(refundFee) && (
|
||||||
|
<Fragment>
|
||||||
|
<Part
|
||||||
|
big
|
||||||
|
title={<i18n.Translate>Refund amount</i18n.Translate>}
|
||||||
|
text={<Amount value={transaction.totalRefundRaw} />}
|
||||||
|
kind="neutral"
|
||||||
|
/>
|
||||||
|
<Part
|
||||||
|
title={<i18n.Translate>Refund fee</i18n.Translate>}
|
||||||
|
text={<Amount value={refundFee} />}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
{pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && (
|
||||||
|
<Part
|
||||||
|
big
|
||||||
|
title={<i18n.Translate>Refund pending</i18n.Translate>}
|
||||||
|
text={<Amount value={pendingRefund} />}
|
||||||
|
kind="positive"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||||
text={transaction.info.merchant.name}
|
text={transaction.info.merchant.name}
|
||||||
@ -447,18 +494,22 @@ export function TransactionView({
|
|||||||
text={<Amount value={transaction.amountEffective} />}
|
text={<Amount value={transaction.amountEffective} />}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
<Part
|
{Amounts.isNonZero(fee) && (
|
||||||
big
|
<Fragment>
|
||||||
title={<i18n.Translate>Deposit amount</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={transaction.amountRaw} />}
|
big
|
||||||
kind="positive"
|
title={<i18n.Translate>Deposit amount</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={transaction.amountRaw} />}
|
||||||
<Part
|
kind="positive"
|
||||||
big
|
/>
|
||||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={fee} />}
|
big
|
||||||
kind="negative"
|
title={<i18n.Translate>Fee</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={fee} />}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
{payto && <PartPayto big payto={payto} kind="neutral" />}
|
{payto && <PartPayto big payto={payto} kind="neutral" />}
|
||||||
</TransactionTemplate>
|
</TransactionTemplate>
|
||||||
);
|
);
|
||||||
@ -485,18 +536,22 @@ export function TransactionView({
|
|||||||
text={<Amount value={transaction.amountEffective} />}
|
text={<Amount value={transaction.amountEffective} />}
|
||||||
kind="negative"
|
kind="negative"
|
||||||
/>
|
/>
|
||||||
<Part
|
{Amounts.isNonZero(fee) && (
|
||||||
big
|
<Fragment>
|
||||||
title={<i18n.Translate>Refresh amount</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={transaction.amountRaw} />}
|
big
|
||||||
kind="neutral"
|
title={<i18n.Translate>Refresh amount</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={transaction.amountRaw} />}
|
||||||
<Part
|
kind="neutral"
|
||||||
big
|
/>
|
||||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={fee} />}
|
big
|
||||||
kind="negative"
|
title={<i18n.Translate>Fee</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={fee} />}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</TransactionTemplate>
|
</TransactionTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -522,18 +577,22 @@ export function TransactionView({
|
|||||||
text={<Amount value={transaction.amountRaw} />}
|
text={<Amount value={transaction.amountRaw} />}
|
||||||
kind="positive"
|
kind="positive"
|
||||||
/>
|
/>
|
||||||
<Part
|
{Amounts.isNonZero(fee) && (
|
||||||
big
|
<Fragment>
|
||||||
title={<i18n.Translate>Received amount</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={transaction.amountEffective} />}
|
big
|
||||||
kind="neutral"
|
title={<i18n.Translate>Received amount</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={transaction.amountEffective} />}
|
||||||
<Part
|
kind="neutral"
|
||||||
big
|
/>
|
||||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={fee} />}
|
big
|
||||||
kind="negative"
|
title={<i18n.Translate>Fee</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={fee} />}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</TransactionTemplate>
|
</TransactionTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -559,37 +618,42 @@ export function TransactionView({
|
|||||||
text={<Amount value={transaction.amountEffective} />}
|
text={<Amount value={transaction.amountEffective} />}
|
||||||
kind="positive"
|
kind="positive"
|
||||||
/>
|
/>
|
||||||
<Part
|
{Amounts.isNonZero(fee) && (
|
||||||
big
|
<Fragment>
|
||||||
title={<i18n.Translate>Refund amount</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={transaction.amountRaw} />}
|
big
|
||||||
kind="neutral"
|
title={<i18n.Translate>Refund amount</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={transaction.amountRaw} />}
|
||||||
<Part
|
kind="neutral"
|
||||||
big
|
/>
|
||||||
title={<i18n.Translate>Fee</i18n.Translate>}
|
<Part
|
||||||
text={<Amount value={fee} />}
|
big
|
||||||
kind="negative"
|
title={<i18n.Translate>Fee</i18n.Translate>}
|
||||||
/>
|
text={<Amount value={fee} />}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Merchant</i18n.Translate>}
|
title={<i18n.Translate>Merchant</i18n.Translate>}
|
||||||
text={transaction.info.merchant.name}
|
text={transaction.info.merchant.name}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Part
|
<Part
|
||||||
title={<i18n.Translate>Purchase</i18n.Translate>}
|
title={<i18n.Translate>Purchase</i18n.Translate>}
|
||||||
text={
|
text={
|
||||||
transaction.info.fulfillmentUrl ? (
|
<a
|
||||||
<a
|
href={Pages.balance_transaction.replace(
|
||||||
href={transaction.info.fulfillmentUrl}
|
":tid",
|
||||||
target="_bank"
|
transaction.refundedTransactionId,
|
||||||
rel="noreferrer"
|
)}
|
||||||
>
|
// href={transaction.info.fulfillmentUrl}
|
||||||
{transaction.info.summary}
|
// target="_bank"
|
||||||
</a>
|
// rel="noreferrer"
|
||||||
) : (
|
>
|
||||||
transaction.info.summary
|
{transaction.info.summary}
|
||||||
)
|
</a>
|
||||||
}
|
}
|
||||||
kind="neutral"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user