gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated (f7299a1a -> d9b73a30)


From: gnunet
Subject: [taler-wallet-core] branch master updated (f7299a1a -> d9b73a30)
Date: Wed, 19 Aug 2020 17:26:48 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a change to branch master
in repository wallet-core.

    from f7299a1a make basic withdrawal and payment work again with new API
     new 082498b2 use /paid API for proof of purchase
     new d9b73a30 test case for /paid API, implement fault-injected merchant

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../taler-integrationtests/src/faultInjection.ts   |  41 +++++
 packages/taler-integrationtests/src/harness.ts     | 112 +++++++-----
 packages/taler-integrationtests/src/helpers.ts     |  93 ++++++++++
 .../src/scenario-prompt-payment.ts                 |   6 +-
 .../src/scenario-rerun-payment-multiple.ts         |   7 +-
 .../src/test-merchant-longpolling.ts               |   6 +-
 .../src/{test-paywall-flow.ts => test-pay-paid.ts} | 114 +++---------
 .../src/test-payment-fault.ts                      |   7 +-
 .../src/test-payment-idempotency.ts                |   8 +-
 .../src/test-payment-multiple.ts                   |   7 +-
 .../taler-integrationtests/src/test-payment.ts     |   8 +-
 .../src/test-paywall-flow.ts                       |  16 +-
 .../src/test-refund-incremental.ts                 |  14 +-
 packages/taler-integrationtests/src/test-refund.ts |  10 +-
 packages/taler-wallet-core/src/operations/pay.ts   | 197 +++++++++++++++------
 packages/taler-wallet-core/src/types/dbTypes.ts    |   2 +
 packages/taler-wallet-core/src/util/http.ts        |   9 +
 17 files changed, 424 insertions(+), 233 deletions(-)
 copy packages/taler-integrationtests/src/{test-paywall-flow.ts => 
test-pay-paid.ts} (59%)

diff --git a/packages/taler-integrationtests/src/faultInjection.ts 
b/packages/taler-integrationtests/src/faultInjection.ts
index 46ab1c5f..a85b1dd7 100644
--- a/packages/taler-integrationtests/src/faultInjection.ts
+++ b/packages/taler-integrationtests/src/faultInjection.ts
@@ -30,7 +30,15 @@ import {
   ExchangeService,
   BankService,
   ExchangeServiceInterface,
+  MerchantServiceInterface,
+  MerchantService,
+  PrivateOrderStatusQuery,
 } from "./harness";
+import {
+  PostOrderRequest,
+  PostOrderResponse,
+  MerchantOrderPrivateStatusResponse,
+} from "./merchantApiTypes";
 
 export interface FaultProxyConfig {
   inboundPort: number;
@@ -220,3 +228,36 @@ export class FaultInjectedExchangeService implements 
ExchangeServiceInterface {
     this.port = proxyInboundPort;
   }
 }
+
+export class FaultInjectedMerchantService implements MerchantServiceInterface {
+  baseUrl: string;
+  port: number;
+  faultProxy: FaultProxy;
+
+  get name(): string {
+    return this.innerMerchant.name;
+  }
+
+  private innerMerchant: MerchantService;
+  private inboundPort: number;
+
+  constructor(
+    t: GlobalTestState,
+    m: MerchantService,
+    proxyInboundPort: number,
+  ) {
+    this.innerMerchant = m;
+    this.faultProxy = new FaultProxy(t, {
+      inboundPort: proxyInboundPort,
+      targetPort: m.port,
+    });
+    this.faultProxy.start();
+    this.inboundPort = proxyInboundPort;
+  }
+
+  makeInstanceBaseUrl(instanceName?: string | undefined): string {
+    const url = new URL(this.innerMerchant.makeInstanceBaseUrl(instanceName));
+    url.port = `${this.inboundPort}`;
+    return url.href;
+  }
+}
diff --git a/packages/taler-integrationtests/src/harness.ts 
b/packages/taler-integrationtests/src/harness.ts
index df2d9f2e..77914af6 100644
--- a/packages/taler-integrationtests/src/harness.ts
+++ b/packages/taler-integrationtests/src/harness.ts
@@ -909,7 +909,62 @@ export interface PrivateOrderStatusQuery {
   sessionId?: string;
 }
 
-export class MerchantService {
+export interface MerchantServiceInterface {
+  makeInstanceBaseUrl(instanceName?: string): string;
+  readonly port: number;
+  readonly name: string;
+}
+
+export namespace MerchantPrivateApi {
+  export async function createOrder(
+    merchantService: MerchantServiceInterface,
+    instanceName: string,
+    req: PostOrderRequest,
+  ): Promise<PostOrderResponse> {
+    const baseUrl = merchantService.makeInstanceBaseUrl(instanceName);
+    let url = new URL("private/orders", baseUrl);
+    const resp = await axios.post(url.href, req);
+    return codecForPostOrderResponse().decode(resp.data);
+  }
+
+  export async function queryPrivateOrderStatus(
+    merchantService: MerchantServiceInterface,
+    query: PrivateOrderStatusQuery,
+  ): Promise<MerchantOrderPrivateStatusResponse> {
+    const reqUrl = new URL(
+      `private/orders/${query.orderId}`,
+      merchantService.makeInstanceBaseUrl(query.instance),
+    );
+    if (query.sessionId) {
+      reqUrl.searchParams.set("session_id", query.sessionId);
+    }
+    const resp = await axios.get(reqUrl.href);
+    return codecForMerchantOrderPrivateStatusResponse().decode(resp.data);
+  }
+
+  export async function giveRefund(
+    merchantService: MerchantServiceInterface,
+    r: {
+    instance: string;
+    orderId: string;
+    amount: string;
+    justification: string;
+  }): Promise<{ talerRefundUri: string }> {
+    const reqUrl = new URL(
+      `private/orders/${r.orderId}/refund`,
+      merchantService.makeInstanceBaseUrl(r.instance),
+    );
+    const resp = await axios.post(reqUrl.href, {
+      refund: r.amount,
+      reason: r.justification,
+    });
+    return {
+      talerRefundUri: resp.data.taler_refund_uri,
+    };
+  }
+}
+
+export class MerchantService implements MerchantServiceInterface {
   static fromExistingConfig(gc: GlobalTestState, name: string) {
     const cfgFilename = gc.testDir + `/merchant-${name}.conf`;
     const config = Configuration.load(cfgFilename);
@@ -930,6 +985,14 @@ export class MerchantService {
     private configFilename: string,
   ) {}
 
+  get port(): number {
+    return this.merchantConfig.httpPort;
+  }
+
+  get name(): string {
+    return this.merchantConfig.name;
+  }
+
   async start(): Promise<void> {
     await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
 
@@ -1005,20 +1068,6 @@ export class MerchantService {
     });
   }
 
-  async queryPrivateOrderStatus(
-    query: PrivateOrderStatusQuery,
-  ): Promise<MerchantOrderPrivateStatusResponse> {
-    const reqUrl = new URL(
-      `private/orders/${query.orderId}`,
-      this.makeInstanceBaseUrl(query.instance),
-    );
-    if (query.sessionId) {
-      reqUrl.searchParams.set("session_id", query.sessionId);
-    }
-    const resp = await axios.get(reqUrl.href);
-    return codecForMerchantOrderPrivateStatusResponse().decode(resp.data);
-  }
-
   makeInstanceBaseUrl(instanceName?: string): string {
     if (instanceName === undefined || instanceName === "default") {
       return `http://localhost:${this.merchantConfig.httpPort}/`;
@@ -1027,39 +1076,6 @@ export class MerchantService {
     }
   }
 
-  async giveRefund(r: {
-    instance: string;
-    orderId: string;
-    amount: string;
-    justification: string;
-  }): Promise<{ talerRefundUri: string }> {
-    const reqUrl = new URL(
-      `private/orders/${r.orderId}/refund`,
-      this.makeInstanceBaseUrl(r.instance),
-    );
-    const resp = await axios.post(reqUrl.href, {
-      refund: r.amount,
-      reason: r.justification,
-    });
-    return {
-      talerRefundUri: resp.data.taler_refund_uri,
-    };
-  }
-
-  async createOrder(
-    instanceName: string,
-    req: PostOrderRequest,
-  ): Promise<PostOrderResponse> {
-    let url;
-    if (instanceName === "default") {
-      url = `http://localhost:${this.merchantConfig.httpPort}/private/orders`;
-    } else {
-      url = 
`http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/private/orders`;
-    }
-    const resp = await axios.post(url, req);
-    return codecForPostOrderResponse().decode(resp.data);
-  }
-
   async pingUntilAvailable(): Promise<void> {
     const url = `http://localhost:${this.merchantConfig.httpPort}/config`;
     await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`);
diff --git a/packages/taler-integrationtests/src/helpers.ts 
b/packages/taler-integrationtests/src/helpers.ts
index b37b6b96..86f530e0 100644
--- a/packages/taler-integrationtests/src/helpers.ts
+++ b/packages/taler-integrationtests/src/helpers.ts
@@ -33,8 +33,10 @@ import {
   BankService,
   defaultCoinConfig,
   ExchangeBankAccount,
+  MerchantServiceInterface,
 } from "./harness";
 import { AmountString } from "taler-wallet-core/lib/types/talerTypes";
+import { FaultInjectedMerchantService } from "./faultInjection";
 
 export interface SimpleTestEnvironment {
   commonDb: DbInfo;
@@ -123,6 +125,97 @@ export async function createSimpleTestkudosEnvironment(
   };
 }
 
+export interface FaultyMerchantTestEnvironment {
+  commonDb: DbInfo;
+  bank: BankService;
+  exchange: ExchangeService;
+  exchangeBankAccount: ExchangeBankAccount;
+  merchant: MerchantService;
+  faultyMerchant: FaultInjectedMerchantService;
+  wallet: WalletCli;
+}
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ */
+export async function createFaultInjectedMerchantTestkudosEnvironment(
+  t: GlobalTestState,
+): Promise<FaultyMerchantTestEnvironment> {
+  const db = await setupDb(t);
+
+  const bank = await BankService.create(t, {
+    allowRegistrations: true,
+    currency: "TESTKUDOS",
+    database: db.connStr,
+    httpPort: 8082,
+  });
+
+  const exchange = ExchangeService.create(t, {
+    name: "testexchange-1",
+    currency: "TESTKUDOS",
+    httpPort: 8081,
+    database: db.connStr,
+  });
+
+  const merchant = await MerchantService.create(t, {
+    name: "testmerchant-1",
+    currency: "TESTKUDOS",
+    httpPort: 8083,
+    database: db.connStr,
+  });
+
+  const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083);
+
+  const exchangeBankAccount = await bank.createExchangeAccount(
+    "MyExchange",
+    "x",
+  );
+  exchange.addBankAccount("1", exchangeBankAccount);
+
+  bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+  await bank.start();
+
+  await bank.pingUntilAvailable();
+
+  exchange.addOfferedCoins(defaultCoinConfig);
+
+  await exchange.start();
+  await exchange.pingUntilAvailable();
+
+  merchant.addExchange(exchange);
+
+  await merchant.start();
+  await merchant.pingUntilAvailable();
+
+  await merchant.addInstance({
+    id: "minst1",
+    name: "minst1",
+    paytoUris: ["payto://x-taler-bank/minst1"],
+  });
+
+  await merchant.addInstance({
+    id: "default",
+    name: "Default Instance",
+    paytoUris: [`payto://x-taler-bank/merchant-default`],
+  });
+
+  console.log("setup done!");
+
+  const wallet = new WalletCli(t);
+
+  return {
+    commonDb: db,
+    exchange,
+    merchant,
+    wallet,
+    bank,
+    exchangeBankAccount,
+    faultyMerchant,
+  };
+}
+
 /**
  * Withdraw balance.
  */
diff --git a/packages/taler-integrationtests/src/scenario-prompt-payment.ts 
b/packages/taler-integrationtests/src/scenario-prompt-payment.ts
index 3e4bfc6c..3c34075d 100644
--- a/packages/taler-integrationtests/src/scenario-prompt-payment.ts
+++ b/packages/taler-integrationtests/src/scenario-prompt-payment.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState } from "./harness";
+import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness";
 import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
 
 /**
@@ -39,7 +39,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -47,7 +47,7 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
diff --git 
a/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts 
b/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts
index 525ba9a2..3a98987b 100644
--- a/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts
+++ b/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts
@@ -24,6 +24,7 @@ import {
   MerchantService,
   WalletCli,
   runTestWithState,
+  MerchantPrivateApi,
 } from "./harness";
 import { withdrawViaBank } from "./helpers";
 import fs from "fs";
@@ -52,7 +53,7 @@ async function withdrawAndPay(
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:80",
@@ -60,7 +61,7 @@ async function withdrawAndPay(
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
@@ -81,7 +82,7 @@ async function withdrawAndPay(
 
   // Check if payment was successful.
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
diff --git a/packages/taler-integrationtests/src/test-merchant-longpolling.ts 
b/packages/taler-integrationtests/src/test-merchant-longpolling.ts
index 664926c0..1470cadf 100644
--- a/packages/taler-integrationtests/src/test-merchant-longpolling.ts
+++ b/packages/taler-integrationtests/src/test-merchant-longpolling.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState } from "./harness";
+import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness";
 import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
 import {
   PreparePayResultType,
@@ -53,7 +53,7 @@ runTest(async (t: GlobalTestState) => {
    * =========================================================================
    */
 
-  let orderResp = await merchant.createOrder("default", {
+  let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -63,7 +63,7 @@ runTest(async (t: GlobalTestState) => {
 
   const firstOrderId = orderResp.order_id;
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
     sessionId: "mysession-one",
   });
diff --git a/packages/taler-integrationtests/src/test-paywall-flow.ts 
b/packages/taler-integrationtests/src/test-pay-paid.ts
similarity index 59%
copy from packages/taler-integrationtests/src/test-paywall-flow.ts
copy to packages/taler-integrationtests/src/test-pay-paid.ts
index 74be96c8..eb58fce3 100644
--- a/packages/taler-integrationtests/src/test-paywall-flow.ts
+++ b/packages/taler-integrationtests/src/test-pay-paid.ts
@@ -17,14 +17,16 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState } from "./harness";
-import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
+import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness";
+import { createSimpleTestkudosEnvironment, withdrawViaBank, 
createFaultInjectedMerchantTestkudosEnvironment } from "./helpers";
 import {
   PreparePayResultType,
   codecForMerchantOrderStatusUnpaid,
   ConfirmPayResultType,
+  URL,
 } from "taler-wallet-core";
 import axios from "axios";
+import { FaultInjectionRequestContext } from "./faultInjection";
 
 /**
  * Run test for basic, bank-integrated withdrawal.
@@ -36,8 +38,8 @@ runTest(async (t: GlobalTestState) => {
     wallet,
     bank,
     exchange,
-    merchant,
-  } = await createSimpleTestkudosEnvironment(t);
+    faultyMerchant,
+  } = await createFaultInjectedMerchantTestkudosEnvironment(t);
 
   // Withdraw digital cash into the wallet.
 
@@ -52,7 +54,9 @@ runTest(async (t: GlobalTestState) => {
    * =========================================================================
    */
 
-  let orderResp = await merchant.createOrder("default", {
+  const merchant = faultyMerchant;
+
+  let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -60,9 +64,8 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  const firstOrderId = orderResp.order_id;
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
     sessionId: "mysession-one",
   });
@@ -131,11 +134,11 @@ runTest(async (t: GlobalTestState) => {
 
   /**
    * =========================================================================
-   * Now change up the session ID!
+   * Now change up the session ID and do payment re-play!
    * =========================================================================
    */
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
     sessionId: "mysession-two",
   });
@@ -145,6 +148,20 @@ runTest(async (t: GlobalTestState) => {
 
   publicOrderStatusUrl = orderStatus.order_status_url;
 
+  let numPayRequested = 0;
+  let numPaidRequested = 0;
+
+  faultyMerchant.faultProxy.addFault({
+    modifyRequest(ctx: FaultInjectionRequestContext) {
+      const url = new URL(ctx.requestUrl);
+      if (url.pathname.endsWith("/pay")) {
+        numPayRequested++;
+      } else if (url.pathname.endsWith("/paid")) {
+        numPaidRequested++;
+      }
+    }
+  });
+
   // Pay with new taler://pay URI, which should
   // have the new session ID!
   // Wallet should now automatically re-play payment.
@@ -155,81 +172,8 @@ runTest(async (t: GlobalTestState) => {
   t.assertTrue(preparePayResp.status === 
PreparePayResultType.AlreadyConfirmed);
   t.assertTrue(preparePayResp.paid);
 
-  /**
-   * =========================================================================
-   * Now we test re-purchase detection.
-   * =========================================================================
-   */
-
-  orderResp = await merchant.createOrder("default", {
-    order: {
-      summary: "Buy me!",
-      amount: "TESTKUDOS:5",
-      // Same fulfillment URL as previously!
-      fulfillment_url: "https://example.com/article42";,
-    },
-  });
-
-  const secondOrderId = orderResp.order_id;
-
-  orderStatus = await merchant.queryPrivateOrderStatus({
-    orderId: secondOrderId,
-    sessionId: "mysession-three",
-  });
-
-  t.assertTrue(orderStatus.order_status === "unpaid");
-
-  t.assertTrue(orderStatus.already_paid_order_id === undefined);
-  publicOrderStatusUrl = orderStatus.order_status_url;
-
-  // Here the re-purchase detection should kick in,
-  // and the wallet should re-pay for the old order
-  // under the new session ID (mysession-three).
-  preparePayResp = await wallet.preparePay({
-    talerPayUri: orderStatus.taler_pay_uri,
-  });
-
-  t.assertTrue(preparePayResp.status === 
PreparePayResultType.AlreadyConfirmed);
-  t.assertTrue(preparePayResp.paid);
-
-  // The first order should now be paid under "mysession-three",
-  // as the wallet did re-purchase detection
-  orderStatus = await merchant.queryPrivateOrderStatus({
-    orderId: firstOrderId,
-    sessionId: "mysession-three",
-  });
-
-  t.assertTrue(orderStatus.order_status === "paid");
-
-  // Check that with a completely new session ID, the status would NOT
-  // be paid.
-  orderStatus = await merchant.queryPrivateOrderStatus({
-    orderId: firstOrderId,
-    sessionId: "mysession-four",
-  });
-
-  t.assertTrue(orderStatus.order_status === "unpaid");
-
-  // Now check if the public status of the new order is correct.
-
-  console.log("requesting public status", publicOrderStatusUrl);
-
-  // Ask the order status of the claimed-but-unpaid order
-  publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
-    validateStatus: () => true,
-  });
-
-  if (publicOrderStatusResp.status != 402) {
-    throw Error(
-      `expected status 402, but got ${publicOrderStatusResp.status}`,
-    );
-  }
-
-  pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
-    publicOrderStatusResp.data,
-  );
-
-  console.log(publicOrderStatusResp.data);
+  // Make sure the wallet is actually doing the replay properly.
+  t.assertTrue(numPaidRequested == 1);
+  t.assertTrue(numPayRequested == 0);
 
-  t.assertTrue(pubUnpaidStatus.already_paid_order_id === firstOrderId);
 });
diff --git a/packages/taler-integrationtests/src/test-payment-fault.ts 
b/packages/taler-integrationtests/src/test-payment-fault.ts
index 4babdc50..8065b430 100644
--- a/packages/taler-integrationtests/src/test-payment-fault.ts
+++ b/packages/taler-integrationtests/src/test-payment-fault.ts
@@ -30,6 +30,7 @@ import {
   BankService,
   WalletCli,
   defaultCoinConfig,
+  MerchantPrivateApi,
 } from "./harness";
 import {
   FaultInjectedExchangeService,
@@ -145,7 +146,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -153,7 +154,7 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
@@ -195,7 +196,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Check if payment was successful.
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
diff --git a/packages/taler-integrationtests/src/test-payment-idempotency.ts 
b/packages/taler-integrationtests/src/test-payment-idempotency.ts
index bc641a35..6c75de6b 100644
--- a/packages/taler-integrationtests/src/test-payment-idempotency.ts
+++ b/packages/taler-integrationtests/src/test-payment-idempotency.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState } from "./harness";
+import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness";
 import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
 import { PreparePayResultType } from "taler-wallet-core";
 
@@ -41,7 +41,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -49,7 +49,7 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
@@ -84,7 +84,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Check if payment was successful.
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
diff --git a/packages/taler-integrationtests/src/test-payment-multiple.ts 
b/packages/taler-integrationtests/src/test-payment-multiple.ts
index 00b3c0b6..8be8a90f 100644
--- a/packages/taler-integrationtests/src/test-payment-multiple.ts
+++ b/packages/taler-integrationtests/src/test-payment-multiple.ts
@@ -27,6 +27,7 @@ import {
   WalletCli,
   coin_ct10,
   coin_u1,
+  MerchantPrivateApi,
 } from "./harness";
 import { withdrawViaBank } from "./helpers";
 
@@ -122,7 +123,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:80",
@@ -130,7 +131,7 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
@@ -151,7 +152,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Check if payment was successful.
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
diff --git a/packages/taler-integrationtests/src/test-payment.ts 
b/packages/taler-integrationtests/src/test-payment.ts
index 12b4267b..d2cbb10f 100644
--- a/packages/taler-integrationtests/src/test-payment.ts
+++ b/packages/taler-integrationtests/src/test-payment.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState } from "./harness";
+import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness";
 import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
 import { PreparePayResultType } from "taler-wallet-core";
 
@@ -40,7 +40,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -48,7 +48,7 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
@@ -70,7 +70,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Check if payment was successful.
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
diff --git a/packages/taler-integrationtests/src/test-paywall-flow.ts 
b/packages/taler-integrationtests/src/test-paywall-flow.ts
index 74be96c8..c4c5784e 100644
--- a/packages/taler-integrationtests/src/test-paywall-flow.ts
+++ b/packages/taler-integrationtests/src/test-paywall-flow.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState } from "./harness";
+import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness";
 import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
 import {
   PreparePayResultType,
@@ -52,7 +52,7 @@ runTest(async (t: GlobalTestState) => {
    * =========================================================================
    */
 
-  let orderResp = await merchant.createOrder("default", {
+  let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -62,7 +62,7 @@ runTest(async (t: GlobalTestState) => {
 
   const firstOrderId = orderResp.order_id;
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
     sessionId: "mysession-one",
   });
@@ -135,7 +135,7 @@ runTest(async (t: GlobalTestState) => {
    * =========================================================================
    */
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
     sessionId: "mysession-two",
   });
@@ -161,7 +161,7 @@ runTest(async (t: GlobalTestState) => {
    * =========================================================================
    */
 
-  orderResp = await merchant.createOrder("default", {
+  orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -172,7 +172,7 @@ runTest(async (t: GlobalTestState) => {
 
   const secondOrderId = orderResp.order_id;
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: secondOrderId,
     sessionId: "mysession-three",
   });
@@ -194,7 +194,7 @@ runTest(async (t: GlobalTestState) => {
 
   // The first order should now be paid under "mysession-three",
   // as the wallet did re-purchase detection
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: firstOrderId,
     sessionId: "mysession-three",
   });
@@ -203,7 +203,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Check that with a completely new session ID, the status would NOT
   // be paid.
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: firstOrderId,
     sessionId: "mysession-four",
   });
diff --git a/packages/taler-integrationtests/src/test-refund-incremental.ts 
b/packages/taler-integrationtests/src/test-refund-incremental.ts
index 59a36b94..c3d2b783 100644
--- a/packages/taler-integrationtests/src/test-refund-incremental.ts
+++ b/packages/taler-integrationtests/src/test-refund-incremental.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState, delayMs } from "./harness";
+import { runTest, GlobalTestState, delayMs, MerchantPrivateApi } from 
"./harness";
 import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
 
 /**
@@ -39,7 +39,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -47,7 +47,7 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
@@ -68,13 +68,13 @@ runTest(async (t: GlobalTestState) => {
 
   // Check if payment was successful.
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
   t.assertTrue(orderStatus.order_status === "paid");
 
-  let ref = await merchant.giveRefund({
+  let ref = await MerchantPrivateApi.giveRefund(merchant, {
     amount: "TESTKUDOS:2.5",
     instance: "default",
     justification: "foo",
@@ -87,7 +87,7 @@ runTest(async (t: GlobalTestState) => {
   // refund will be grouped with the previous one.
   await delayMs(1.2);
 
-  ref = await merchant.giveRefund({
+  ref = await MerchantPrivateApi.giveRefund(merchant, {
     amount: "TESTKUDOS:5",
     instance: "default",
     justification: "bar",
@@ -101,7 +101,7 @@ runTest(async (t: GlobalTestState) => {
   });
   console.log(r);
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
diff --git a/packages/taler-integrationtests/src/test-refund.ts 
b/packages/taler-integrationtests/src/test-refund.ts
index d0d0a0a0..f88a399b 100644
--- a/packages/taler-integrationtests/src/test-refund.ts
+++ b/packages/taler-integrationtests/src/test-refund.ts
@@ -17,7 +17,7 @@
 /**
  * Imports.
  */
-import { runTest, GlobalTestState } from "./harness";
+import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness";
 import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
 
 /**
@@ -39,7 +39,7 @@ runTest(async (t: GlobalTestState) => {
 
   // Set up order.
 
-  const orderResp = await merchant.createOrder("default", {
+  const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
     order: {
       summary: "Buy me!",
       amount: "TESTKUDOS:5",
@@ -47,7 +47,7 @@ runTest(async (t: GlobalTestState) => {
     },
   });
 
-  let orderStatus = await merchant.queryPrivateOrderStatus({
+  let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, 
{
     orderId: orderResp.order_id,
   });
 
@@ -68,13 +68,13 @@ runTest(async (t: GlobalTestState) => {
 
   // Check if payment was successful.
 
-  orderStatus = await merchant.queryPrivateOrderStatus({
+  orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
     orderId: orderResp.order_id,
   });
 
   t.assertTrue(orderStatus.order_status === "paid");
 
-  const ref = await merchant.giveRefund({
+  const ref = await MerchantPrivateApi.giveRefund(merchant, {
     amount: "TESTKUDOS:5",
     instance: "default",
     justification: "foo",
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index 996e1c1e..327a6c80 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -60,7 +60,11 @@ import { createRefreshGroup, getTotalRefreshCost } from 
"./refresh";
 import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state";
 import { getTimestampNow, timestampAddDuration } from "../util/time";
 import { strcmp, canonicalJson } from "../util/helpers";
-import { readSuccessResponseJsonOrThrow } from "../util/http";
+import {
+  readSuccessResponseJsonOrThrow,
+  throwUnexpectedRequestError,
+  getHttpResponseErrorDetails,
+} from "../util/http";
 import { TalerErrorCode } from "../TalerErrorCode";
 import { URL } from "../util/url";
 
@@ -457,6 +461,7 @@ async function recordConfirmPay(
     autoRefundDeadline: undefined,
     paymentSubmitPending: true,
     refunds: {},
+    merchantPaySig: undefined,
   };
 
   await ws.db.runWithWriteTransaction(
@@ -769,6 +774,89 @@ async function startDownloadProposal(
   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(
   ws: InternalWalletState,
   proposalId: string,
@@ -784,71 +872,66 @@ export async function submitPay(
 
   logger.trace("paying with session ID", sessionId);
 
-  const payUrl = new URL(
-    `orders/${purchase.contractData.orderId}/pay`,
-    purchase.contractData.merchantBaseUrl,
-  ).href;
+  if (!purchase.merchantPaySig) {
+    const payUrl = new URL(
+      `orders/${purchase.contractData.orderId}/pay`,
+      purchase.contractData.merchantBaseUrl,
+    ).href;
 
-  const reqBody = {
-    coins: purchase.coinDepositPermissions,
-    session_id: purchase.lastSessionId,
-  };
+    const reqBody = {
+      coins: purchase.coinDepositPermissions,
+      session_id: purchase.lastSessionId,
+    };
 
-  logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2));
+    logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2));
 
-  const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
-    ws.http.postJson(payUrl, reqBody),
-  );
+    const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
+      ws.http.postJson(payUrl, reqBody),
+    );
 
-  const merchantResp = await readSuccessResponseJsonOrThrow(
-    resp,
-    codecForMerchantPayResponse(),
-  );
+    const merchantResp = await readSuccessResponseJsonOrThrow(
+      resp,
+      codecForMerchantPayResponse(),
+    );
 
-  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 valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
+      merchantResp.sig,
+      purchase.contractData.contractTermsHash,
+      merchantPub,
+    );
 
-  const merchantPub = purchase.contractData.merchantPub;
-  const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
-    merchantResp.sig,
-    purchase.contractData.contractTermsHash,
-    merchantPub,
-  );
-  if (!valid) {
-    logger.error("merchant payment signature invalid");
-    // FIXME: properly display error
-    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);
+    if (!valid) {
+      logger.error("merchant payment signature invalid");
+      // FIXME: properly display error
+      throw Error("merchant payment signature invalid");
     }
-  }
 
-  await ws.db.runWithWriteTransaction(
-    [Stores.purchases, Stores.payEvents],
-    async (tx) => {
-      await tx.put(Stores.purchases, purchase);
-      const payEvent: PayEventRecord = {
-        proposalId,
-        sessionId,
-        timestamp: now,
-        isReplay: !isFirst,
-      };
-      await tx.put(Stores.payEvents, payEvent);
-    },
-  );
+    await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
+  } else {
+    const payAgainUrl = new URL(
+      `orders/${purchase.contractData.orderId}/paid`,
+      purchase.contractData.merchantBaseUrl,
+    ).href;
+    const reqBody = {
+      sig: purchase.merchantPaySig,
+      h_contract: purchase.contractData.contractTermsHash,
+      session_id: sessionId ?? "",
+    };
+    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);
   ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts 
b/packages/taler-wallet-core/src/types/dbTypes.ts
index 26e636e4..42192dd9 100644
--- a/packages/taler-wallet-core/src/types/dbTypes.ts
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -1309,6 +1309,8 @@ export interface PurchaseRecord {
    */
   timestampFirstSuccessfulPay: Timestamp | undefined;
 
+  merchantPaySig: string | undefined;
+
   /**
    * When was the purchase made?
    * Refers to the time that the user accepted.
diff --git a/packages/taler-wallet-core/src/util/http.ts 
b/packages/taler-wallet-core/src/util/http.ts
index 72de2ed1..22566daa 100644
--- a/packages/taler-wallet-core/src/util/http.ts
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -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(
   httpResponse: HttpResponse,
   talerErrorResponse: TalerErrorResponse,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]