wallet-core: fix tipping state machine issues
This commit is contained in:
parent
2f4f43cc1f
commit
1ee9ef80bd
@ -876,7 +876,9 @@ export interface TipRecord {
|
||||
export enum TipRecordStatus {
|
||||
PendingPickup = 10,
|
||||
|
||||
SuspendidPickup = 21,
|
||||
SuspendidPickup = 20,
|
||||
|
||||
DialogAccept = 30,
|
||||
|
||||
Done = 50,
|
||||
Aborted = 51,
|
||||
|
@ -532,21 +532,23 @@ async function processDownloadProposal(
|
||||
h: contractTermsHash,
|
||||
contractTermsRaw: proposalResp.contract_terms,
|
||||
});
|
||||
if (
|
||||
const isResourceFulfillmentUrl =
|
||||
fulfillmentUrl &&
|
||||
(fulfillmentUrl.startsWith("http://") ||
|
||||
fulfillmentUrl.startsWith("https://"))
|
||||
) {
|
||||
const differentPurchase =
|
||||
await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
|
||||
// FIXME: Adjust this to account for refunds, don't count as repurchase
|
||||
// if original order is refunded.
|
||||
if (differentPurchase) {
|
||||
logger.warn("repurchase detected");
|
||||
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
|
||||
p.repurchaseProposalId = differentPurchase.proposalId;
|
||||
await tx.purchases.put(p);
|
||||
}
|
||||
fulfillmentUrl.startsWith("https://"));
|
||||
let otherPurchase: PurchaseRecord | undefined;
|
||||
if (isResourceFulfillmentUrl) {
|
||||
otherPurchase = await tx.purchases.indexes.byFulfillmentUrl.get(
|
||||
fulfillmentUrl,
|
||||
);
|
||||
}
|
||||
// FIXME: Adjust this to account for refunds, don't count as repurchase
|
||||
// if original order is refunded.
|
||||
if (otherPurchase) {
|
||||
logger.warn("repurchase detected");
|
||||
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
|
||||
p.repurchaseProposalId = otherPurchase.proposalId;
|
||||
await tx.purchases.put(p);
|
||||
} else {
|
||||
p.purchaseStatus = PurchaseStatus.DialogProposed;
|
||||
await tx.purchases.put(p);
|
||||
@ -602,6 +604,7 @@ async function createPurchase(
|
||||
(!noncePriv || oldProposal.noncePriv === noncePriv) &&
|
||||
oldProposal.claimToken === claimToken
|
||||
) {
|
||||
// FIXME: This lacks proper error handling
|
||||
await processDownloadProposal(ws, oldProposal.proposalId);
|
||||
return oldProposal.proposalId;
|
||||
}
|
||||
|
@ -34,7 +34,6 @@ import {
|
||||
PrepareTipResult,
|
||||
TalerErrorCode,
|
||||
TalerPreciseTimestamp,
|
||||
TalerProtocolTimestamp,
|
||||
TipPlanchetDetail,
|
||||
TransactionAction,
|
||||
TransactionMajorState,
|
||||
@ -102,17 +101,21 @@ export function computeTipTransactionStatus(
|
||||
major: TransactionMajorState.Pending,
|
||||
minor: TransactionMinorState.Pickup,
|
||||
};
|
||||
case TipRecordStatus.DialogAccept:
|
||||
return {
|
||||
major: TransactionMajorState.Dialog,
|
||||
minor: TransactionMinorState.Proposed,
|
||||
};
|
||||
case TipRecordStatus.SuspendidPickup:
|
||||
return {
|
||||
major: TransactionMajorState.Pending,
|
||||
minor: TransactionMinorState.User,
|
||||
minor: TransactionMinorState.Pickup,
|
||||
};
|
||||
default:
|
||||
assertUnreachable(tipRecord.status);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function computeTipTransactionActions(
|
||||
tipRecord: TipRecord,
|
||||
): TransactionAction[] {
|
||||
@ -125,6 +128,8 @@ export function computeTipTransactionActions(
|
||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||
case TipRecordStatus.SuspendidPickup:
|
||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||
case TipRecordStatus.DialogAccept:
|
||||
return [TransactionAction.Abort];
|
||||
default:
|
||||
assertUnreachable(tipRecord.status);
|
||||
}
|
||||
@ -190,7 +195,7 @@ export async function prepareTip(
|
||||
const newTipRecord: TipRecord = {
|
||||
walletTipId: walletTipId,
|
||||
acceptedTimestamp: undefined,
|
||||
status: TipRecordStatus.PendingPickup,
|
||||
status: TipRecordStatus.DialogAccept,
|
||||
tipAmountRaw: Amounts.stringify(amount),
|
||||
tipExpiration: tipPickupStatus.expiration,
|
||||
exchangeBaseUrl: tipPickupStatus.exchange_url,
|
||||
@ -234,7 +239,6 @@ export async function prepareTip(
|
||||
export async function processTip(
|
||||
ws: InternalWalletState,
|
||||
walletTipId: string,
|
||||
options: Record<string, never> = {},
|
||||
): Promise<OperationAttemptResult> {
|
||||
const tipRecord = await ws.db
|
||||
.mktx((x) => [x.tips])
|
||||
@ -248,14 +252,22 @@ export async function processTip(
|
||||
};
|
||||
}
|
||||
|
||||
if (tipRecord.pickedUpTimestamp) {
|
||||
logger.warn("tip already picked up");
|
||||
return {
|
||||
type: OperationAttemptResultType.Finished,
|
||||
result: undefined,
|
||||
};
|
||||
switch (tipRecord.status) {
|
||||
case TipRecordStatus.Aborted:
|
||||
case TipRecordStatus.DialogAccept:
|
||||
case TipRecordStatus.Done:
|
||||
case TipRecordStatus.SuspendidPickup:
|
||||
return {
|
||||
type: OperationAttemptResultType.Finished,
|
||||
result: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const transactionId = constructTransactionIdentifier({
|
||||
tag: TransactionType.Tip,
|
||||
walletTipId,
|
||||
});
|
||||
|
||||
const denomsForWithdraw = tipRecord.denomsSel;
|
||||
|
||||
const planchets: DerivedTipPlanchet[] = [];
|
||||
@ -391,22 +403,27 @@ export async function processTip(
|
||||
});
|
||||
}
|
||||
|
||||
await ws.db
|
||||
const transitionInfo = await ws.db
|
||||
.mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips])
|
||||
.runReadWrite(async (tx) => {
|
||||
const tr = await tx.tips.get(walletTipId);
|
||||
if (!tr) {
|
||||
return;
|
||||
}
|
||||
if (tr.pickedUpTimestamp) {
|
||||
if (tr.status !== TipRecordStatus.PendingPickup) {
|
||||
return;
|
||||
}
|
||||
const oldTxState = computeTipTransactionStatus(tr);
|
||||
tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
|
||||
tr.status = TipRecordStatus.Done;
|
||||
await tx.tips.put(tr);
|
||||
const newTxState = computeTipTransactionStatus(tr);
|
||||
for (const cr of newCoinRecords) {
|
||||
await makeCoinAvailable(ws, tx, cr);
|
||||
}
|
||||
return { oldTxState, newTxState };
|
||||
});
|
||||
notifyTransition(ws, transactionId, transitionInfo);
|
||||
|
||||
return {
|
||||
type: OperationAttemptResultType.Finished,
|
||||
@ -416,33 +433,46 @@ export async function processTip(
|
||||
|
||||
export async function acceptTip(
|
||||
ws: InternalWalletState,
|
||||
tipId: string,
|
||||
walletTipId: string,
|
||||
): Promise<AcceptTipResponse> {
|
||||
const found = await ws.db
|
||||
const transactionId = constructTransactionIdentifier({
|
||||
tag: TransactionType.Tip,
|
||||
walletTipId,
|
||||
});
|
||||
const dbRes = await ws.db
|
||||
.mktx((x) => [x.tips])
|
||||
.runReadWrite(async (tx) => {
|
||||
const tipRecord = await tx.tips.get(tipId);
|
||||
const tipRecord = await tx.tips.get(walletTipId);
|
||||
if (!tipRecord) {
|
||||
logger.error("tip not found");
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
if (tipRecord.status != TipRecordStatus.DialogAccept) {
|
||||
logger.warn("Unable to accept tip in the current state");
|
||||
return { tipRecord };
|
||||
}
|
||||
const oldTxState = computeTipTransactionStatus(tipRecord);
|
||||
tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now();
|
||||
tipRecord.status = TipRecordStatus.PendingPickup;
|
||||
await tx.tips.put(tipRecord);
|
||||
|
||||
return tipRecord;
|
||||
const newTxState = computeTipTransactionStatus(tipRecord);
|
||||
return { tipRecord, transitionInfo: { oldTxState, newTxState } };
|
||||
});
|
||||
|
||||
if (found) {
|
||||
await processTip(ws, tipId);
|
||||
if (!dbRes) {
|
||||
throw Error("tip not found");
|
||||
}
|
||||
//FIXME: if tip is not found the behavior of the function is the same
|
||||
// as the tip was found and finished
|
||||
|
||||
notifyTransition(ws, transactionId, dbRes.transitionInfo);
|
||||
|
||||
const tipRecord = dbRes.tipRecord;
|
||||
|
||||
return {
|
||||
transactionId: constructTransactionIdentifier({
|
||||
tag: TransactionType.Tip,
|
||||
walletTipId: tipId,
|
||||
walletTipId: walletTipId,
|
||||
}),
|
||||
next_url: found?.next_url,
|
||||
next_url: tipRecord.next_url,
|
||||
};
|
||||
}
|
||||
|
||||
@ -472,10 +502,12 @@ export async function suspendTipTransaction(
|
||||
case TipRecordStatus.Done:
|
||||
case TipRecordStatus.SuspendidPickup:
|
||||
case TipRecordStatus.Aborted:
|
||||
case TipRecordStatus.DialogAccept:
|
||||
break;
|
||||
case TipRecordStatus.PendingPickup:
|
||||
newStatus = TipRecordStatus.SuspendidPickup;
|
||||
break;
|
||||
|
||||
default:
|
||||
assertUnreachable(tipRec.status);
|
||||
}
|
||||
@ -519,14 +551,13 @@ export async function resumeTipTransaction(
|
||||
let newStatus: TipRecordStatus | undefined = undefined;
|
||||
switch (tipRec.status) {
|
||||
case TipRecordStatus.Done:
|
||||
case TipRecordStatus.PendingPickup:
|
||||
case TipRecordStatus.Aborted:
|
||||
case TipRecordStatus.DialogAccept:
|
||||
break;
|
||||
case TipRecordStatus.SuspendidPickup:
|
||||
newStatus = TipRecordStatus.PendingPickup;
|
||||
break;
|
||||
case TipRecordStatus.PendingPickup:
|
||||
break;
|
||||
case TipRecordStatus.Aborted:
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(tipRec.status);
|
||||
}
|
||||
@ -577,14 +608,13 @@ export async function abortTipTransaction(
|
||||
let newStatus: TipRecordStatus | undefined = undefined;
|
||||
switch (tipRec.status) {
|
||||
case TipRecordStatus.Done:
|
||||
case TipRecordStatus.Aborted:
|
||||
case TipRecordStatus.PendingPickup:
|
||||
case TipRecordStatus.DialogAccept:
|
||||
break;
|
||||
case TipRecordStatus.SuspendidPickup:
|
||||
newStatus = TipRecordStatus.Aborted;
|
||||
break;
|
||||
case TipRecordStatus.PendingPickup:
|
||||
break;
|
||||
case TipRecordStatus.Aborted:
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(tipRec.status);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user