wallet-core,harness: implement pay templating
This commit is contained in:
parent
a9073a6797
commit
04ab9f3780
@ -38,6 +38,7 @@ import {
|
|||||||
hash,
|
hash,
|
||||||
j2s,
|
j2s,
|
||||||
Logger,
|
Logger,
|
||||||
|
MerchantTemplateAddDetails,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
TalerProtocolDuration,
|
TalerProtocolDuration,
|
||||||
@ -66,15 +67,15 @@ import { CoinConfig } from "./denomStructures.js";
|
|||||||
import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
|
import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
|
||||||
import {
|
import {
|
||||||
codecForMerchantOrderPrivateStatusResponse,
|
codecForMerchantOrderPrivateStatusResponse,
|
||||||
codecForPostOrderResponse,
|
codecForMerchantPostOrderResponse,
|
||||||
MerchantInstancesResponse,
|
MerchantInstancesResponse,
|
||||||
MerchantOrderPrivateStatusResponse,
|
MerchantOrderPrivateStatusResponse,
|
||||||
PostOrderRequest,
|
MerchantPostOrderRequest,
|
||||||
PostOrderResponse,
|
MerchantPostOrderResponse,
|
||||||
TipCreateConfirmation,
|
TipCreateConfirmation,
|
||||||
TipCreateRequest,
|
TipCreateRequest,
|
||||||
TippingReserveStatus,
|
TippingReserveStatus,
|
||||||
} from "./merchantApiTypes.js";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
createRemoteWallet,
|
createRemoteWallet,
|
||||||
getClientFromRemoteWallet,
|
getClientFromRemoteWallet,
|
||||||
@ -1473,15 +1474,31 @@ export namespace MerchantPrivateApi {
|
|||||||
export async function createOrder(
|
export async function createOrder(
|
||||||
merchantService: MerchantServiceInterface,
|
merchantService: MerchantServiceInterface,
|
||||||
instanceName: string,
|
instanceName: string,
|
||||||
req: PostOrderRequest,
|
req: MerchantPostOrderRequest,
|
||||||
withAuthorization: WithAuthorization = {},
|
withAuthorization: WithAuthorization = {},
|
||||||
): Promise<PostOrderResponse> {
|
): Promise<MerchantPostOrderResponse> {
|
||||||
const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
|
const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
|
||||||
let url = new URL("private/orders", baseUrl);
|
let url = new URL("private/orders", baseUrl);
|
||||||
const resp = await axios.post(url.href, req, {
|
const resp = await axios.post(url.href, req, {
|
||||||
headers: withAuthorization as Record<string, string>,
|
headers: withAuthorization as Record<string, string>,
|
||||||
});
|
});
|
||||||
return codecForPostOrderResponse().decode(resp.data);
|
return codecForMerchantPostOrderResponse().decode(resp.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTemplate(
|
||||||
|
merchantService: MerchantServiceInterface,
|
||||||
|
instanceName: string,
|
||||||
|
req: MerchantTemplateAddDetails,
|
||||||
|
withAuthorization: WithAuthorization = {},
|
||||||
|
) {
|
||||||
|
const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
|
||||||
|
let url = new URL("private/templates", baseUrl);
|
||||||
|
const resp = await axios.post(url.href, req, {
|
||||||
|
headers: withAuthorization as Record<string, string>,
|
||||||
|
});
|
||||||
|
if (resp.status !== 204) {
|
||||||
|
throw Error("failed to create template");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryPrivateOrderStatus(
|
export async function queryPrivateOrderStatus(
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2020 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import { ConfirmPayResultType, Duration, PreparePayResultType, TalerProtocolTimestamp } from "@gnu-taler/taler-util";
|
||||||
|
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
|
||||||
|
import {
|
||||||
|
createSimpleTestkudosEnvironment,
|
||||||
|
withdrawViaBank,
|
||||||
|
makeTestPayment,
|
||||||
|
} from "../harness/helpers.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test for taler://payment-template/ URIs
|
||||||
|
*/
|
||||||
|
export async function runPaymentTemplateTest(t: GlobalTestState) {
|
||||||
|
// Set up test environment
|
||||||
|
|
||||||
|
const { wallet, bank, exchange, merchant } =
|
||||||
|
await createSimpleTestkudosEnvironment(t);
|
||||||
|
|
||||||
|
await MerchantPrivateApi.createTemplate(merchant, "default", {
|
||||||
|
template_id: "template1",
|
||||||
|
template_description: "my test template",
|
||||||
|
template_contract: {
|
||||||
|
minimum_age: 0,
|
||||||
|
pay_duration: Duration.toTalerProtocolDuration(
|
||||||
|
Duration.fromSpec({
|
||||||
|
minutes: 2,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
summary: "hello, I'm a summary",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Withdraw digital cash into the wallet.
|
||||||
|
|
||||||
|
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
|
||||||
|
|
||||||
|
// Request a template payment
|
||||||
|
|
||||||
|
const preparePayResult = await wallet.client.call(
|
||||||
|
WalletApiOperation.PreparePayForTemplate,
|
||||||
|
{
|
||||||
|
talerPayTemplateUri: `taler+http://pay-template/localhost:${merchant.port}/template1?amount=TESTKUDOS:1`,
|
||||||
|
templateParams: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(preparePayResult);
|
||||||
|
|
||||||
|
t.assertTrue(
|
||||||
|
preparePayResult.status === PreparePayResultType.PaymentPossible,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pay for it
|
||||||
|
|
||||||
|
const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, {
|
||||||
|
proposalId: preparePayResult.proposalId,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.assertTrue(r2.type === ConfirmPayResultType.Done);
|
||||||
|
|
||||||
|
// Check if payment was successful.
|
||||||
|
|
||||||
|
const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
|
||||||
|
merchant,
|
||||||
|
{
|
||||||
|
orderId: preparePayResult.contractTerms.order_id,
|
||||||
|
instance: "default",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
t.assertTrue(orderStatus.order_status === "paid");
|
||||||
|
|
||||||
|
await wallet.runUntilDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
runPaymentTemplateTest.suites = ["wallet"];
|
@ -100,6 +100,7 @@ import { runKycTest } from "./test-kyc.js";
|
|||||||
import { runPaymentAbortTest } from "./test-payment-abort.js";
|
import { runPaymentAbortTest } from "./test-payment-abort.js";
|
||||||
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
|
import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js";
|
||||||
import { runWalletBalanceTest } from "./test-wallet-balance.js";
|
import { runWalletBalanceTest } from "./test-wallet-balance.js";
|
||||||
|
import { runPaymentTemplateTest } from "./test-payment-template.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test runner.
|
* Test runner.
|
||||||
@ -163,6 +164,7 @@ const allTests: TestMainFunction[] = [
|
|||||||
runPaymentIdempotencyTest,
|
runPaymentIdempotencyTest,
|
||||||
runPaymentMultipleTest,
|
runPaymentMultipleTest,
|
||||||
runPaymentTest,
|
runPaymentTest,
|
||||||
|
runPaymentTemplateTest,
|
||||||
runPaymentAbortTest,
|
runPaymentAbortTest,
|
||||||
runPaymentTransientTest,
|
runPaymentTransientTest,
|
||||||
runPaymentZeroTest,
|
runPaymentZeroTest,
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"typeRoots": ["./node_modules/@types"]
|
"typeRoots": ["./node_modules/@types"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*", "../taler-util/src/merchant-api-types.ts"],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../taler-wallet-core/"
|
"path": "../taler-wallet-core/"
|
||||||
|
@ -35,3 +35,4 @@ export { RequestThrottler } from "./RequestThrottler.js";
|
|||||||
export * from "./CancellationToken.js";
|
export * from "./CancellationToken.js";
|
||||||
export * from "./contract-terms.js";
|
export * from "./contract-terms.js";
|
||||||
export * from "./base64.js";
|
export * from "./base64.js";
|
||||||
|
export * from "./merchant-api-types.js";
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
MerchantContractTerms,
|
MerchantContractTerms,
|
||||||
Duration,
|
|
||||||
Codec,
|
Codec,
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
codecForString,
|
codecForString,
|
||||||
@ -47,7 +46,7 @@ import {
|
|||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
export interface PostOrderRequest {
|
export interface MerchantPostOrderRequest {
|
||||||
// The order must at least contain the minimal
|
// The order must at least contain the minimal
|
||||||
// order detail, but can override all
|
// order detail, but can override all
|
||||||
order: Partial<MerchantContractTerms>;
|
order: Partial<MerchantContractTerms>;
|
||||||
@ -71,18 +70,18 @@ export interface PostOrderRequest {
|
|||||||
|
|
||||||
export type ClaimToken = string;
|
export type ClaimToken = string;
|
||||||
|
|
||||||
export interface PostOrderResponse {
|
export interface MerchantPostOrderResponse {
|
||||||
order_id: string;
|
order_id: string;
|
||||||
token?: ClaimToken;
|
token?: ClaimToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForPostOrderResponse = (): Codec<PostOrderResponse> =>
|
export const codecForMerchantPostOrderResponse = (): Codec<MerchantPostOrderResponse> =>
|
||||||
buildCodecForObject<PostOrderResponse>()
|
buildCodecForObject<MerchantPostOrderResponse>()
|
||||||
.property("order_id", codecForString())
|
.property("order_id", codecForString())
|
||||||
.property("token", codecOptional(codecForString()))
|
.property("token", codecOptional(codecForString()))
|
||||||
.build("PostOrderResponse");
|
.build("PostOrderResponse");
|
||||||
|
|
||||||
export const codecForRefundDetails = (): Codec<RefundDetails> =>
|
export const codecForMerchantRefundDetails = (): Codec<RefundDetails> =>
|
||||||
buildCodecForObject<RefundDetails>()
|
buildCodecForObject<RefundDetails>()
|
||||||
.property("reason", codecForString())
|
.property("reason", codecForString())
|
||||||
.property("pending", codecForBoolean())
|
.property("pending", codecForBoolean())
|
||||||
@ -90,9 +89,9 @@ export const codecForRefundDetails = (): Codec<RefundDetails> =>
|
|||||||
.property("timestamp", codecForTimestamp)
|
.property("timestamp", codecForTimestamp)
|
||||||
.build("PostOrderResponse");
|
.build("PostOrderResponse");
|
||||||
|
|
||||||
export const codecForCheckPaymentPaidResponse =
|
export const codecForMerchantCheckPaymentPaidResponse =
|
||||||
(): Codec<CheckPaymentPaidResponse> =>
|
(): Codec<MerchantCheckPaymentPaidResponse> =>
|
||||||
buildCodecForObject<CheckPaymentPaidResponse>()
|
buildCodecForObject<MerchantCheckPaymentPaidResponse>()
|
||||||
.property("order_status_url", codecForString())
|
.property("order_status_url", codecForString())
|
||||||
.property("order_status", codecForConstString("paid"))
|
.property("order_status", codecForConstString("paid"))
|
||||||
.property("refunded", codecForBoolean())
|
.property("refunded", codecForBoolean())
|
||||||
@ -128,13 +127,13 @@ export const codecForMerchantOrderPrivateStatusResponse =
|
|||||||
(): Codec<MerchantOrderPrivateStatusResponse> =>
|
(): Codec<MerchantOrderPrivateStatusResponse> =>
|
||||||
buildCodecForUnion<MerchantOrderPrivateStatusResponse>()
|
buildCodecForUnion<MerchantOrderPrivateStatusResponse>()
|
||||||
.discriminateOn("order_status")
|
.discriminateOn("order_status")
|
||||||
.alternative("paid", codecForCheckPaymentPaidResponse())
|
.alternative("paid", codecForMerchantCheckPaymentPaidResponse())
|
||||||
.alternative("unpaid", codecForCheckPaymentUnpaidResponse())
|
.alternative("unpaid", codecForCheckPaymentUnpaidResponse())
|
||||||
.alternative("claimed", codecForCheckPaymentClaimedResponse())
|
.alternative("claimed", codecForCheckPaymentClaimedResponse())
|
||||||
.build("MerchantOrderPrivateStatusResponse");
|
.build("MerchantOrderPrivateStatusResponse");
|
||||||
|
|
||||||
export type MerchantOrderPrivateStatusResponse =
|
export type MerchantOrderPrivateStatusResponse =
|
||||||
| CheckPaymentPaidResponse
|
| MerchantCheckPaymentPaidResponse
|
||||||
| CheckPaymentUnpaidResponse
|
| CheckPaymentUnpaidResponse
|
||||||
| CheckPaymentClaimedResponse;
|
| CheckPaymentClaimedResponse;
|
||||||
|
|
||||||
@ -145,7 +144,7 @@ export interface CheckPaymentClaimedResponse {
|
|||||||
contract_terms: MerchantContractTerms;
|
contract_terms: MerchantContractTerms;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckPaymentPaidResponse {
|
export interface MerchantCheckPaymentPaidResponse {
|
||||||
// did the customer pay for this contract
|
// did the customer pay for this contract
|
||||||
order_status: "paid";
|
order_status: "paid";
|
||||||
|
|
||||||
@ -334,3 +333,36 @@ export interface MerchantInstanceDetail {
|
|||||||
// front-ends do not have to support wallets selecting payment targets.
|
// front-ends do not have to support wallets selecting payment targets.
|
||||||
payment_targets: string[];
|
payment_targets: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MerchantTemplateContractDetails {
|
||||||
|
// Human-readable summary for the template.
|
||||||
|
summary?: string;
|
||||||
|
|
||||||
|
// The price is imposed by the merchant and cannot be changed by the customer.
|
||||||
|
// This parameter is optional.
|
||||||
|
amount?: AmountString;
|
||||||
|
|
||||||
|
// Minimum age buyer must have (in years). Default is 0.
|
||||||
|
minimum_age: number;
|
||||||
|
|
||||||
|
// The time the customer need to pay before his order will be deleted.
|
||||||
|
// It is deleted if the customer did not pay and if the duration is over.
|
||||||
|
pay_duration: TalerProtocolDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MerchantTemplateAddDetails {
|
||||||
|
|
||||||
|
// Template ID to use.
|
||||||
|
template_id: string;
|
||||||
|
|
||||||
|
// Human-readable description for the template.
|
||||||
|
template_description: string;
|
||||||
|
|
||||||
|
// A base64-encoded image selected by the merchant.
|
||||||
|
// This parameter is optional.
|
||||||
|
// We are not sure about it.
|
||||||
|
image?: string;
|
||||||
|
|
||||||
|
// Additional information in a separate template.
|
||||||
|
template_contract: MerchantTemplateContractDetails;
|
||||||
|
}
|
@ -1481,10 +1481,11 @@ export const codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
|
|||||||
.property("ev_sig", codecForBlindedDenominationSignature())
|
.property("ev_sig", codecForBlindedDenominationSignature())
|
||||||
.build("WithdrawResponse");
|
.build("WithdrawResponse");
|
||||||
|
|
||||||
export const codecForWithdrawBatchResponse = (): Codec<ExchangeWithdrawBatchResponse> =>
|
export const codecForWithdrawBatchResponse =
|
||||||
buildCodecForObject<ExchangeWithdrawBatchResponse>()
|
(): Codec<ExchangeWithdrawBatchResponse> =>
|
||||||
.property("ev_sigs", codecForList(codecForWithdrawResponse()))
|
buildCodecForObject<ExchangeWithdrawBatchResponse>()
|
||||||
.build("WithdrawBatchResponse");
|
.property("ev_sigs", codecForList(codecForWithdrawResponse()))
|
||||||
|
.build("WithdrawBatchResponse");
|
||||||
|
|
||||||
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
|
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
|
||||||
buildCodecForObject<MerchantPayResponse>()
|
buildCodecForObject<MerchantPayResponse>()
|
||||||
@ -1757,7 +1758,6 @@ export interface ExchangeBatchWithdrawRequest {
|
|||||||
planchets: ExchangeWithdrawRequest[];
|
planchets: ExchangeWithdrawRequest[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ExchangeRefreshRevealRequest {
|
export interface ExchangeRefreshRevealRequest {
|
||||||
new_denoms_h: HashCodeString[];
|
new_denoms_h: HashCodeString[];
|
||||||
coin_evs: CoinEnvelope[];
|
coin_evs: CoinEnvelope[];
|
||||||
@ -2113,3 +2113,8 @@ export const codecForWalletKycUuid = (): Codec<WalletKycUuid> =>
|
|||||||
.property("requirement_row", codecForNumber())
|
.property("requirement_row", codecForNumber())
|
||||||
.property("h_payto", codecForString())
|
.property("h_payto", codecForString())
|
||||||
.build("WalletKycUuid");
|
.build("WalletKycUuid");
|
||||||
|
|
||||||
|
export interface MerchantUsingTemplateDetails {
|
||||||
|
summary?: string;
|
||||||
|
amount?: AmountString;
|
||||||
|
}
|
||||||
|
@ -22,6 +22,8 @@ import {
|
|||||||
parseTipUri,
|
parseTipUri,
|
||||||
parsePayPushUri,
|
parsePayPushUri,
|
||||||
constructPayPushUri,
|
constructPayPushUri,
|
||||||
|
parsePayTemplateUri,
|
||||||
|
constructPayUri,
|
||||||
} from "./taleruri.js";
|
} from "./taleruri.js";
|
||||||
|
|
||||||
test("taler pay url parsing: wrong scheme", (t) => {
|
test("taler pay url parsing: wrong scheme", (t) => {
|
||||||
@ -225,3 +227,37 @@ test("taler peer to peer push URI (construction)", (t) => {
|
|||||||
});
|
});
|
||||||
t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
|
t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("taler pay URI (construction)", (t) => {
|
||||||
|
const url1 = constructPayUri("http://localhost:123/", "foo", "");
|
||||||
|
t.deepEqual(url1, "taler+http://pay/localhost:123/foo/");
|
||||||
|
|
||||||
|
const url2 = constructPayUri("http://localhost:123/", "foo", "bla");
|
||||||
|
t.deepEqual(url2, "taler+http://pay/localhost:123/foo/bla");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("taler pay template URI (parsing)", (t) => {
|
||||||
|
const url1 =
|
||||||
|
"taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5";
|
||||||
|
const r1 = parsePayTemplateUri(url1);
|
||||||
|
if (!r1) {
|
||||||
|
t.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.deepEqual(r1.merchantBaseUrl, "https://merchant.example.com/");
|
||||||
|
t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
|
||||||
|
t.deepEqual(r1.templateParams.amount, "KUDOS:5");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("taler pay template URI (parsing, http with port)", (t) => {
|
||||||
|
const url1 =
|
||||||
|
"taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5";
|
||||||
|
const r1 = parsePayTemplateUri(url1);
|
||||||
|
if (!r1) {
|
||||||
|
t.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.deepEqual(r1.merchantBaseUrl, "http://merchant.example.com:1234/");
|
||||||
|
t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
|
||||||
|
t.deepEqual(r1.templateParams.amount, "KUDOS:5");
|
||||||
|
});
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import { BackupRecovery } from "./backup-types.js";
|
import { BackupRecovery } from "./backup-types.js";
|
||||||
import { canonicalizeBaseUrl } from "./helpers.js";
|
import { canonicalizeBaseUrl } from "./helpers.js";
|
||||||
import { initNodePrng } from "./prng-node.js";
|
|
||||||
import { URLSearchParams, URL } from "./url.js";
|
import { URLSearchParams, URL } from "./url.js";
|
||||||
|
|
||||||
export interface PayUriResult {
|
export interface PayUriResult {
|
||||||
@ -27,6 +26,12 @@ export interface PayUriResult {
|
|||||||
noncePriv: string | undefined;
|
noncePriv: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PayTemplateUriResult {
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
templateId: string;
|
||||||
|
templateParams: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WithdrawUriResult {
|
export interface WithdrawUriResult {
|
||||||
bankIntegrationApiBaseUrl: string;
|
bankIntegrationApiBaseUrl: string;
|
||||||
withdrawalOperationId: string;
|
withdrawalOperationId: string;
|
||||||
@ -91,6 +96,7 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
|
|||||||
|
|
||||||
export enum TalerUriType {
|
export enum TalerUriType {
|
||||||
TalerPay = "taler-pay",
|
TalerPay = "taler-pay",
|
||||||
|
TalerTemplate = "taler-template",
|
||||||
TalerWithdraw = "taler-withdraw",
|
TalerWithdraw = "taler-withdraw",
|
||||||
TalerTip = "taler-tip",
|
TalerTip = "taler-tip",
|
||||||
TalerRefund = "taler-refund",
|
TalerRefund = "taler-refund",
|
||||||
@ -103,6 +109,7 @@ export enum TalerUriType {
|
|||||||
|
|
||||||
const talerActionPayPull = "pay-pull";
|
const talerActionPayPull = "pay-pull";
|
||||||
const talerActionPayPush = "pay-push";
|
const talerActionPayPush = "pay-push";
|
||||||
|
const talerActionPayTemplate = "pay-template";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Classify a taler:// URI.
|
* Classify a taler:// URI.
|
||||||
@ -121,6 +128,12 @@ export function classifyTalerUri(s: string): TalerUriType {
|
|||||||
if (sl.startsWith("taler+http://pay/")) {
|
if (sl.startsWith("taler+http://pay/")) {
|
||||||
return TalerUriType.TalerPay;
|
return TalerUriType.TalerPay;
|
||||||
}
|
}
|
||||||
|
if (sl.startsWith("taler://pay-template/")) {
|
||||||
|
return TalerUriType.TalerPay;
|
||||||
|
}
|
||||||
|
if (sl.startsWith("taler+http://pay-template/")) {
|
||||||
|
return TalerUriType.TalerPay;
|
||||||
|
}
|
||||||
if (sl.startsWith("taler://tip/")) {
|
if (sl.startsWith("taler://tip/")) {
|
||||||
return TalerUriType.TalerTip;
|
return TalerUriType.TalerTip;
|
||||||
}
|
}
|
||||||
@ -216,6 +229,38 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parsePayTemplateUri(
|
||||||
|
s: string,
|
||||||
|
): PayTemplateUriResult | undefined {
|
||||||
|
const pi = parseProtoInfo(s, talerActionPayTemplate);
|
||||||
|
if (!pi) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const c = pi?.rest.split("?");
|
||||||
|
const q = new URLSearchParams(c[1] ?? "");
|
||||||
|
const parts = c[0].split("/");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const host = parts[0].toLowerCase();
|
||||||
|
const templateId = parts[parts.length - 1];
|
||||||
|
const pathSegments = parts.slice(1, parts.length - 1);
|
||||||
|
const p = [host, ...pathSegments].join("/");
|
||||||
|
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
|
||||||
|
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
|
q.forEach((v, k) => {
|
||||||
|
params[k] = v;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
merchantBaseUrl,
|
||||||
|
templateId,
|
||||||
|
templateParams: params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function constructPayUri(
|
export function constructPayUri(
|
||||||
merchantBaseUrl: string,
|
merchantBaseUrl: string,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
@ -227,9 +272,21 @@ export function constructPayUri(
|
|||||||
const url = new URL(base);
|
const url = new URL(base);
|
||||||
const isHttp = base.startsWith("http://");
|
const isHttp = base.startsWith("http://");
|
||||||
let result = isHttp ? `taler+http://pay/` : `taler://pay/`;
|
let result = isHttp ? `taler+http://pay/` : `taler://pay/`;
|
||||||
result += `${url.hostname}${url.pathname}${orderId}/${sessionId}?`;
|
result += url.hostname;
|
||||||
if (claimToken) result += `c=${claimToken}`;
|
if (url.port != "") {
|
||||||
if (noncePriv) result += `n=${noncePriv}`;
|
result += `:${url.port}`;
|
||||||
|
}
|
||||||
|
result += `${url.pathname}${orderId}/${sessionId}`;
|
||||||
|
let queryPart = "";
|
||||||
|
if (claimToken) {
|
||||||
|
queryPart += `c=${claimToken}`;
|
||||||
|
}
|
||||||
|
if (noncePriv) {
|
||||||
|
queryPart += `n=${noncePriv}`;
|
||||||
|
}
|
||||||
|
if (queryPart) {
|
||||||
|
result += "?" + queryPart;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1418,6 +1418,17 @@ export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
|
|||||||
.property("talerPayUri", codecForString())
|
.property("talerPayUri", codecForString())
|
||||||
.build("PreparePay");
|
.build("PreparePay");
|
||||||
|
|
||||||
|
export interface PreparePayTemplateRequest {
|
||||||
|
talerPayTemplateUri: string;
|
||||||
|
templateParams: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForPreparePayTemplateRequest = (): Codec<PreparePayTemplateRequest> =>
|
||||||
|
buildCodecForObject<PreparePayTemplateRequest>()
|
||||||
|
.property("talerPayTemplateUri", codecForString())
|
||||||
|
.property("templateParams", codecForAny())
|
||||||
|
.build("PreparePayTemplate");
|
||||||
|
|
||||||
export interface ConfirmPayRequest {
|
export interface ConfirmPayRequest {
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
@ -78,6 +78,7 @@ import {
|
|||||||
PrepareDepositResponse,
|
PrepareDepositResponse,
|
||||||
PreparePayRequest,
|
PreparePayRequest,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
|
PreparePayTemplateRequest,
|
||||||
PreparePeerPullPaymentRequest,
|
PreparePeerPullPaymentRequest,
|
||||||
PreparePeerPullPaymentResponse,
|
PreparePeerPullPaymentResponse,
|
||||||
PreparePeerPushPaymentRequest,
|
PreparePeerPushPaymentRequest,
|
||||||
@ -126,6 +127,7 @@ export enum WalletApiOperation {
|
|||||||
WithdrawTestkudos = "withdrawTestkudos",
|
WithdrawTestkudos = "withdrawTestkudos",
|
||||||
WithdrawTestBalance = "withdrawTestBalance",
|
WithdrawTestBalance = "withdrawTestBalance",
|
||||||
PreparePayForUri = "preparePayForUri",
|
PreparePayForUri = "preparePayForUri",
|
||||||
|
PreparePayForTemplate = "preparePayForTemplate",
|
||||||
GetContractTermsDetails = "getContractTermsDetails",
|
GetContractTermsDetails = "getContractTermsDetails",
|
||||||
RunIntegrationTest = "runIntegrationTest",
|
RunIntegrationTest = "runIntegrationTest",
|
||||||
TestCrypto = "testCrypto",
|
TestCrypto = "testCrypto",
|
||||||
@ -313,7 +315,7 @@ export type AcceptManualWithdrawalOp = {
|
|||||||
// group: Merchant Payments
|
// group: Merchant Payments
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare to make a payment
|
* Prepare to make a payment based on a taler://pay/ URI.
|
||||||
*/
|
*/
|
||||||
export type PreparePayForUriOp = {
|
export type PreparePayForUriOp = {
|
||||||
op: WalletApiOperation.PreparePayForUri;
|
op: WalletApiOperation.PreparePayForUri;
|
||||||
@ -321,6 +323,15 @@ export type PreparePayForUriOp = {
|
|||||||
response: PreparePayResult;
|
response: PreparePayResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare to make a payment based on a taler://pay-template/ URI.
|
||||||
|
*/
|
||||||
|
export type PreparePayForTemplateOp = {
|
||||||
|
op: WalletApiOperation.PreparePayForTemplate;
|
||||||
|
request: PreparePayTemplateRequest;
|
||||||
|
response: PreparePayResult;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetContractTermsDetailsOp = {
|
export type GetContractTermsDetailsOp = {
|
||||||
op: WalletApiOperation.GetContractTermsDetails;
|
op: WalletApiOperation.GetContractTermsDetails;
|
||||||
request: GetContractTermsDetailsRequest;
|
request: GetContractTermsDetailsRequest;
|
||||||
@ -835,6 +846,7 @@ export type WalletOperations = {
|
|||||||
[WalletApiOperation.GetVersion]: GetVersionOp;
|
[WalletApiOperation.GetVersion]: GetVersionOp;
|
||||||
[WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp;
|
[WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp;
|
||||||
[WalletApiOperation.PreparePayForUri]: PreparePayForUriOp;
|
[WalletApiOperation.PreparePayForUri]: PreparePayForUriOp;
|
||||||
|
[WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp;
|
||||||
[WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp;
|
[WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp;
|
||||||
[WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp;
|
[WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp;
|
||||||
[WalletApiOperation.ConfirmPay]: ConfirmPayOp;
|
[WalletApiOperation.ConfirmPay]: ConfirmPayOp;
|
||||||
|
@ -56,8 +56,10 @@ import {
|
|||||||
codecForInitiatePeerPushPaymentRequest,
|
codecForInitiatePeerPushPaymentRequest,
|
||||||
codecForIntegrationTestArgs,
|
codecForIntegrationTestArgs,
|
||||||
codecForListKnownBankAccounts,
|
codecForListKnownBankAccounts,
|
||||||
|
codecForMerchantPostOrderResponse,
|
||||||
codecForPrepareDepositRequest,
|
codecForPrepareDepositRequest,
|
||||||
codecForPreparePayRequest,
|
codecForPreparePayRequest,
|
||||||
|
codecForPreparePayTemplateRequest,
|
||||||
codecForPreparePeerPullPaymentRequest,
|
codecForPreparePeerPullPaymentRequest,
|
||||||
codecForPreparePeerPushPaymentRequest,
|
codecForPreparePeerPushPaymentRequest,
|
||||||
codecForPrepareRefundRequest,
|
codecForPrepareRefundRequest,
|
||||||
@ -77,6 +79,7 @@ import {
|
|||||||
CoinDumpJson,
|
CoinDumpJson,
|
||||||
CoinRefreshRequest,
|
CoinRefreshRequest,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
|
constructPayUri,
|
||||||
CoreApiResponse,
|
CoreApiResponse,
|
||||||
DenominationInfo,
|
DenominationInfo,
|
||||||
DenomOperationMap,
|
DenomOperationMap,
|
||||||
@ -88,7 +91,6 @@ import {
|
|||||||
ExchangesListResponse,
|
ExchangesListResponse,
|
||||||
ExchangeTosStatusDetails,
|
ExchangeTosStatusDetails,
|
||||||
FeeDescription,
|
FeeDescription,
|
||||||
GetBalanceDetailRequest,
|
|
||||||
GetExchangeTosResult,
|
GetExchangeTosResult,
|
||||||
InitResponse,
|
InitResponse,
|
||||||
j2s,
|
j2s,
|
||||||
@ -96,7 +98,9 @@ import {
|
|||||||
KnownBankAccountsInfo,
|
KnownBankAccountsInfo,
|
||||||
Logger,
|
Logger,
|
||||||
ManualWithdrawalDetails,
|
ManualWithdrawalDetails,
|
||||||
|
MerchantUsingTemplateDetails,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
|
parsePayTemplateUri,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
@ -156,11 +160,7 @@ import {
|
|||||||
runBackupCycle,
|
runBackupCycle,
|
||||||
} from "./operations/backup/index.js";
|
} from "./operations/backup/index.js";
|
||||||
import { setWalletDeviceId } from "./operations/backup/state.js";
|
import { setWalletDeviceId } from "./operations/backup/state.js";
|
||||||
import {
|
import { getBalanceDetail, getBalances } from "./operations/balance.js";
|
||||||
getBalanceDetail,
|
|
||||||
getBalances,
|
|
||||||
getMerchantPaymentBalanceDetails,
|
|
||||||
} from "./operations/balance.js";
|
|
||||||
import {
|
import {
|
||||||
getExchangeTosStatus,
|
getExchangeTosStatus,
|
||||||
makeExchangeListItem,
|
makeExchangeListItem,
|
||||||
@ -186,7 +186,6 @@ import {
|
|||||||
} from "./operations/exchanges.js";
|
} from "./operations/exchanges.js";
|
||||||
import { getMerchantInfo } from "./operations/merchants.js";
|
import { getMerchantInfo } from "./operations/merchants.js";
|
||||||
import {
|
import {
|
||||||
abortPay as abortPay,
|
|
||||||
applyRefund,
|
applyRefund,
|
||||||
applyRefundFromPurchaseId,
|
applyRefundFromPurchaseId,
|
||||||
confirmPay,
|
confirmPay,
|
||||||
@ -1171,11 +1170,50 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
await runPending(ws, true);
|
await runPending(ws, true);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
// FIXME: Deprecate one of the aliases!
|
|
||||||
case WalletApiOperation.PreparePayForUri: {
|
case WalletApiOperation.PreparePayForUri: {
|
||||||
const req = codecForPreparePayRequest().decode(payload);
|
const req = codecForPreparePayRequest().decode(payload);
|
||||||
return await preparePayForUri(ws, req.talerPayUri);
|
return await preparePayForUri(ws, req.talerPayUri);
|
||||||
}
|
}
|
||||||
|
case WalletApiOperation.PreparePayForTemplate: {
|
||||||
|
const req = codecForPreparePayTemplateRequest().decode(payload);
|
||||||
|
const url = parsePayTemplateUri(req.talerPayTemplateUri);
|
||||||
|
const templateDetails: MerchantUsingTemplateDetails = {};
|
||||||
|
if (!url) {
|
||||||
|
throw Error("invalid taler-template URI");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
url.templateParams.amount &&
|
||||||
|
typeof url.templateParams.amount === "string"
|
||||||
|
) {
|
||||||
|
templateDetails.amount =
|
||||||
|
req.templateParams.amount ?? url.templateParams.amount;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
url.templateParams.summary &&
|
||||||
|
typeof url.templateParams.summary === "string"
|
||||||
|
) {
|
||||||
|
templateDetails.summary =
|
||||||
|
req.templateParams.summary ?? url.templateParams.summary;
|
||||||
|
}
|
||||||
|
const reqUrl = new URL(
|
||||||
|
`templates/${url.templateId}`,
|
||||||
|
url.merchantBaseUrl,
|
||||||
|
);
|
||||||
|
const httpReq = await ws.http.postJson(reqUrl.href, templateDetails);
|
||||||
|
const resp = await readSuccessResponseJsonOrThrow(
|
||||||
|
httpReq,
|
||||||
|
codecForMerchantPostOrderResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const payUri = constructPayUri(
|
||||||
|
url.merchantBaseUrl,
|
||||||
|
resp.order_id,
|
||||||
|
"",
|
||||||
|
resp.token,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await preparePayForUri(ws, payUri);
|
||||||
|
}
|
||||||
case WalletApiOperation.ConfirmPay: {
|
case WalletApiOperation.ConfirmPay: {
|
||||||
const req = codecForConfirmPayRequest().decode(payload);
|
const req = codecForConfirmPayRequest().decode(payload);
|
||||||
return await confirmPay(ws, req.proposalId, req.sessionId);
|
return await confirmPay(ws, req.proposalId, req.sessionId);
|
||||||
@ -1434,6 +1472,8 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
|
|||||||
case WalletApiOperation.GetVersion: {
|
case WalletApiOperation.GetVersion: {
|
||||||
return getVersion(ws);
|
return getVersion(ws);
|
||||||
}
|
}
|
||||||
|
//default:
|
||||||
|
// assertUnreachable(operation);
|
||||||
}
|
}
|
||||||
throw TalerError.fromDetail(
|
throw TalerError.fromDetail(
|
||||||
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
||||||
|
Loading…
Reference in New Issue
Block a user