auto-refund

This commit is contained in:
Florian Dold 2019-12-07 18:42:18 +01:00
parent d634626d7f
commit 165486a112
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 73 additions and 21 deletions

View File

@ -788,7 +788,7 @@ export interface TipRecord {
/** /**
* Timestamp, the tip can't be picked up anymore after this deadline. * Timestamp, the tip can't be picked up anymore after this deadline.
*/ */
deadline: number; deadline: Timestamp;
/** /**
* The exchange that will sign our coins, chosen by the merchant. * The exchange that will sign our coins, chosen by the merchant.
@ -1066,6 +1066,11 @@ export interface PurchaseRecord {
* Last error (or undefined) for querying the refund status with the merchant. * Last error (or undefined) for querying the refund status with the merchant.
*/ */
lastRefundApplyError: OperationError | undefined; lastRefundApplyError: OperationError | undefined;
/**
* Continue querying the refund status until this deadline has expired.
*/
autoRefundDeadline: Timestamp | undefined;
} }
/** /**

View File

@ -24,7 +24,7 @@
import { AmountJson } from "./amounts"; import { AmountJson } from "./amounts";
import * as Amounts from "./amounts"; import * as Amounts from "./amounts";
import { Timestamp } from "../walletTypes"; import { Timestamp, Duration } from "../walletTypes";
/** /**
* Show an amount in a form suitable for the user. * Show an amount in a form suitable for the user.
@ -151,6 +151,30 @@ export function extractTalerStampOrThrow(stamp: string): Timestamp {
return r; return r;
} }
/**
* Extract a duration from a Taler duration string.
*/
export function extractTalerDuration(duration: string): Duration | undefined {
const m = duration.match(/\/?Delay\(([0-9]*)\)\/?/);
if (!m || !m[1]) {
return undefined;
}
return {
d_ms: parseInt(m[1], 10) * 1000,
};
}
/**
* Extract a duration from a Taler duration string.
*/
export function extractTalerDurationOrThrow(duration: string): Duration {
const r = extractTalerDuration(duration);
if (!r) {
throw Error("invalid duration");
}
return r;
}
/** /**
* Check if a timestamp is in the right format. * Check if a timestamp is in the right format.
*/ */
@ -159,18 +183,6 @@ export function timestampCheck(stamp: string): boolean {
} }
/**
* Get a JavaScript Date object from a Taler date string.
* Returns null if input is not in the right format.
*/
export function getTalerStampDate(stamp: string): Date | null {
const sec = getTalerStampSec(stamp);
if (sec == null) {
return null;
}
return new Date(sec * 1000);
}
/** /**
* Compute the hash function of a JSON object. * Compute the hash function of a JSON object.
*/ */

View File

@ -62,6 +62,8 @@ import {
strcmp, strcmp,
canonicalJson, canonicalJson,
extractTalerStampOrThrow, extractTalerStampOrThrow,
extractTalerDurationOrThrow,
extractTalerDuration,
} from "../util/helpers"; } from "../util/helpers";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
@ -359,6 +361,7 @@ async function recordConfirmPay(
lastRefundApplyError: undefined, lastRefundApplyError: undefined,
refundApplyRetryInfo: initRetryInfo(), refundApplyRetryInfo: initRetryInfo(),
firstSuccessfulPayTimestamp: undefined, firstSuccessfulPayTimestamp: undefined,
autoRefundDeadline: undefined,
}; };
await runWithWriteTransaction( await runWithWriteTransaction(
@ -704,9 +707,23 @@ export async function submitPay(
// FIXME: properly display error // FIXME: properly display error
throw Error("merchant payment signature invalid"); throw Error("merchant payment signature invalid");
} }
const isFirst = purchase.firstSuccessfulPayTimestamp === undefined;
purchase.firstSuccessfulPayTimestamp = getTimestampNow(); purchase.firstSuccessfulPayTimestamp = getTimestampNow();
purchase.lastPayError = undefined; purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false); purchase.payRetryInfo = initRetryInfo(false);
if (isFirst) {
const ar = purchase.contractTerms.auto_refund;
if (ar) {
const autoRefundDelay = extractTalerDuration(ar);
if (autoRefundDelay) {
purchase.refundStatusRequested = true;
purchase.autoRefundDeadline = {
t_ms: getTimestampNow().t_ms + autoRefundDelay.d_ms,
}
}
}
}
const modifiedCoins: CoinRecord[] = []; const modifiedCoins: CoinRecord[] = [];
for (const pc of purchase.payReq.coins) { for (const pc of purchase.payReq.coins) {
const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub); const c = await oneShotGet(ws.db, Stores.coins, pc.coin_pub);
@ -1064,11 +1081,6 @@ async function acceptRefundResponse(
return; return;
} }
p.lastRefundStatusTimestamp = getTimestampNow();
p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo();
p.refundStatusRequested = false;
for (const perm of refundPermissions) { for (const perm of refundPermissions) {
if ( if (
!p.refundsPending[perm.merchant_sig] && !p.refundsPending[perm.merchant_sig] &&
@ -1079,6 +1091,29 @@ async function acceptRefundResponse(
} }
} }
// Are we done with querying yet, or do we need to do another round
// after a retry delay?
let queryDone = true;
if (numNewRefunds === 0) {
if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms < getTimestampNow().t_ms) {
queryDone = false;
}
}
if (queryDone) {
p.lastRefundStatusTimestamp = getTimestampNow();
p.lastRefundStatusError = undefined;
p.refundStatusRetryInfo = initRetryInfo();
p.refundStatusRequested = false;
} else {
// No error, but we need to try again!
p.lastRefundStatusTimestamp = getTimestampNow();
p.refundStatusRetryInfo.retryCounter++;
updateRetryInfoTimeout(p.refundStatusRetryInfo);
p.lastRefundStatusError = undefined;
}
if (numNewRefunds) { if (numNewRefunds) {
p.lastRefundApplyError = undefined; p.lastRefundApplyError = undefined;
p.refundApplyRetryInfo = initRetryInfo(); p.refundApplyRetryInfo = initRetryInfo();

View File

@ -23,7 +23,7 @@ import { TipPickupGetResponse, TipPlanchetDetail, TipResponse } from "../talerTy
import * as Amounts from "../util/amounts"; import * as Amounts from "../util/amounts";
import { Stores, PlanchetRecord, WithdrawalSessionRecord, initRetryInfo, updateRetryInfoTimeout } from "../dbTypes"; import { Stores, PlanchetRecord, WithdrawalSessionRecord, initRetryInfo, updateRetryInfoTimeout } from "../dbTypes";
import { getWithdrawDetailsForAmount, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw"; import { getWithdrawDetailsForAmount, getVerifiedWithdrawDenomList, processWithdrawSession } from "./withdraw";
import { getTalerStampSec } from "../util/helpers"; import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers";
import { updateExchangeFromUrl } from "./exchanges"; import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors"; import { guardOperationException } from "./errors";
@ -68,7 +68,7 @@ export async function getTipStatus(
tipId, tipId,
accepted: false, accepted: false,
amount, amount,
deadline: getTalerStampSec(tipPickupStatus.stamp_expire)!, deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
exchangeUrl: tipPickupStatus.exchange_url, exchangeUrl: tipPickupStatus.exchange_url,
merchantBaseUrl: res.merchantBaseUrl, merchantBaseUrl: res.merchantBaseUrl,
nextUrl: undefined, nextUrl: undefined,