use /paid API for proof of purchase
This commit is contained in:
parent
f7299a1aa0
commit
082498b20d
@ -60,7 +60,11 @@ import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
|||||||
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
|
import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
|
||||||
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
import { getTimestampNow, timestampAddDuration } from "../util/time";
|
||||||
import { strcmp, canonicalJson } from "../util/helpers";
|
import { strcmp, canonicalJson } from "../util/helpers";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
import {
|
||||||
|
readSuccessResponseJsonOrThrow,
|
||||||
|
throwUnexpectedRequestError,
|
||||||
|
getHttpResponseErrorDetails,
|
||||||
|
} from "../util/http";
|
||||||
import { TalerErrorCode } from "../TalerErrorCode";
|
import { TalerErrorCode } from "../TalerErrorCode";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
|
|
||||||
@ -457,6 +461,7 @@ async function recordConfirmPay(
|
|||||||
autoRefundDeadline: undefined,
|
autoRefundDeadline: undefined,
|
||||||
paymentSubmitPending: true,
|
paymentSubmitPending: true,
|
||||||
refunds: {},
|
refunds: {},
|
||||||
|
merchantPaySig: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
@ -769,6 +774,89 @@ async function startDownloadProposal(
|
|||||||
return proposalId;
|
return proposalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function storeFirstPaySuccess(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
sessionId: string | undefined,
|
||||||
|
paySig: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const now = getTimestampNow();
|
||||||
|
await ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.purchases, Stores.payEvents],
|
||||||
|
async (tx) => {
|
||||||
|
const purchase = await tx.get(Stores.purchases, proposalId);
|
||||||
|
|
||||||
|
if (!purchase) {
|
||||||
|
logger.warn("purchase does not exist anymore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
|
||||||
|
if (!isFirst) {
|
||||||
|
logger.warn("payment success already stored");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
purchase.timestampFirstSuccessfulPay = now;
|
||||||
|
purchase.paymentSubmitPending = false;
|
||||||
|
purchase.lastPayError = undefined;
|
||||||
|
purchase.lastSessionId = sessionId;
|
||||||
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
|
purchase.merchantPaySig = paySig;
|
||||||
|
if (isFirst) {
|
||||||
|
const ar = purchase.contractData.autoRefund;
|
||||||
|
if (ar) {
|
||||||
|
logger.info("auto_refund present");
|
||||||
|
purchase.refundStatusRequested = true;
|
||||||
|
purchase.refundStatusRetryInfo = initRetryInfo();
|
||||||
|
purchase.lastRefundStatusError = undefined;
|
||||||
|
purchase.autoRefundDeadline = timestampAddDuration(now, ar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.put(Stores.purchases, purchase);
|
||||||
|
const payEvent: PayEventRecord = {
|
||||||
|
proposalId,
|
||||||
|
sessionId,
|
||||||
|
timestamp: now,
|
||||||
|
isReplay: !isFirst,
|
||||||
|
};
|
||||||
|
await tx.put(Stores.payEvents, payEvent);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function storePayReplaySuccess(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
proposalId: string,
|
||||||
|
sessionId: string | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db.runWithWriteTransaction(
|
||||||
|
[Stores.purchases, Stores.payEvents],
|
||||||
|
async (tx) => {
|
||||||
|
const purchase = await tx.get(Stores.purchases, proposalId);
|
||||||
|
|
||||||
|
if (!purchase) {
|
||||||
|
logger.warn("purchase does not exist anymore");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
|
||||||
|
if (isFirst) {
|
||||||
|
throw Error("invalid payment state");
|
||||||
|
}
|
||||||
|
purchase.paymentSubmitPending = false;
|
||||||
|
purchase.lastPayError = undefined;
|
||||||
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
|
purchase.lastSessionId = sessionId;
|
||||||
|
await tx.put(Stores.purchases, purchase);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a payment to the merchant.
|
||||||
|
*
|
||||||
|
* If the wallet has previously paid, it just transmits the merchant's
|
||||||
|
* own signature certifying that the wallet has previously paid.
|
||||||
|
*/
|
||||||
export async function submitPay(
|
export async function submitPay(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
@ -784,6 +872,7 @@ export async function submitPay(
|
|||||||
|
|
||||||
logger.trace("paying with session ID", sessionId);
|
logger.trace("paying with session ID", sessionId);
|
||||||
|
|
||||||
|
if (!purchase.merchantPaySig) {
|
||||||
const payUrl = new URL(
|
const payUrl = new URL(
|
||||||
`orders/${purchase.contractData.orderId}/pay`,
|
`orders/${purchase.contractData.orderId}/pay`,
|
||||||
purchase.contractData.merchantBaseUrl,
|
purchase.contractData.merchantBaseUrl,
|
||||||
@ -807,48 +896,42 @@ export async function submitPay(
|
|||||||
|
|
||||||
logger.trace("got success from pay URL", merchantResp);
|
logger.trace("got success from pay URL", merchantResp);
|
||||||
|
|
||||||
const now = getTimestampNow();
|
|
||||||
|
|
||||||
const merchantPub = purchase.contractData.merchantPub;
|
const merchantPub = purchase.contractData.merchantPub;
|
||||||
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
||||||
merchantResp.sig,
|
merchantResp.sig,
|
||||||
purchase.contractData.contractTermsHash,
|
purchase.contractData.contractTermsHash,
|
||||||
merchantPub,
|
merchantPub,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
logger.error("merchant payment signature invalid");
|
logger.error("merchant payment signature invalid");
|
||||||
// FIXME: properly display error
|
// FIXME: properly display error
|
||||||
throw Error("merchant payment signature invalid");
|
throw Error("merchant payment signature invalid");
|
||||||
}
|
}
|
||||||
const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
|
|
||||||
purchase.timestampFirstSuccessfulPay = now;
|
|
||||||
purchase.paymentSubmitPending = false;
|
|
||||||
purchase.lastPayError = undefined;
|
|
||||||
purchase.payRetryInfo = initRetryInfo(false);
|
|
||||||
if (isFirst) {
|
|
||||||
const ar = purchase.contractData.autoRefund;
|
|
||||||
if (ar) {
|
|
||||||
logger.info("auto_refund present");
|
|
||||||
purchase.refundStatusRequested = true;
|
|
||||||
purchase.refundStatusRetryInfo = initRetryInfo();
|
|
||||||
purchase.lastRefundStatusError = undefined;
|
|
||||||
purchase.autoRefundDeadline = timestampAddDuration(now, ar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
|
||||||
[Stores.purchases, Stores.payEvents],
|
} else {
|
||||||
async (tx) => {
|
const payAgainUrl = new URL(
|
||||||
await tx.put(Stores.purchases, purchase);
|
`orders/${purchase.contractData.orderId}/paid`,
|
||||||
const payEvent: PayEventRecord = {
|
purchase.contractData.merchantBaseUrl,
|
||||||
proposalId,
|
).href;
|
||||||
sessionId,
|
const reqBody = {
|
||||||
timestamp: now,
|
sig: purchase.merchantPaySig,
|
||||||
isReplay: !isFirst,
|
h_contract: purchase.contractData.contractTermsHash,
|
||||||
|
session_id: sessionId ?? "",
|
||||||
};
|
};
|
||||||
await tx.put(Stores.payEvents, payEvent);
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
|
||||||
},
|
ws.http.postJson(payAgainUrl, reqBody),
|
||||||
);
|
);
|
||||||
|
if (resp.status !== 204) {
|
||||||
|
throw OperationFailedError.fromCode(
|
||||||
|
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
|
||||||
|
"/paid failed",
|
||||||
|
getHttpResponseErrorDetails(resp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await storePayReplaySuccess(ws, proposalId, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
const nextUrl = getNextUrl(purchase.contractData);
|
const nextUrl = getNextUrl(purchase.contractData);
|
||||||
ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
|
ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
|
||||||
|
@ -1309,6 +1309,8 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
timestampFirstSuccessfulPay: Timestamp | undefined;
|
timestampFirstSuccessfulPay: Timestamp | undefined;
|
||||||
|
|
||||||
|
merchantPaySig: string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When was the purchase made?
|
* When was the purchase made?
|
||||||
* Refers to the time that the user accepted.
|
* Refers to the time that the user accepted.
|
||||||
|
@ -151,6 +151,15 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHttpResponseErrorDetails(
|
||||||
|
httpResponse: HttpResponse,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
requestUrl: httpResponse.requestUrl,
|
||||||
|
httpStatusCode: httpResponse.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function throwUnexpectedRequestError(
|
export function throwUnexpectedRequestError(
|
||||||
httpResponse: HttpResponse,
|
httpResponse: HttpResponse,
|
||||||
talerErrorResponse: TalerErrorResponse,
|
talerErrorResponse: TalerErrorResponse,
|
||||||
|
Loading…
Reference in New Issue
Block a user