commit 8b0453c244374b6af2e2039a89650cd066e6394e
parent 3a9839c8e24ff3a282e827a59c8dd85739b104ef
Author: Florian Dold <florian@dold.me>
Date: Thu, 27 Nov 2025 16:35:27 +0100
harness,util: extend merchant-wire test, fix API types for merchant
Diffstat:
4 files changed, 155 insertions(+), 66 deletions(-)
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -2128,6 +2128,15 @@ export class MerchantService implements MerchantServiceInterface {
);
}
+ async runReconciliationOnce() {
+ await runCommand(
+ this.globalState,
+ `merchant-${this.name}-reconciliation-once`,
+ "taler-merchant-reconciliation",
+ [...this.timetravelArgArr, "-LINFO", "-c", this.configFilename, "-t"],
+ );
+ }
+
async runKyccheckOnce() {
await runCommand(
this.globalState,
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-wire.ts b/packages/taler-harness/src/integrationtests/test-merchant-wire.ts
@@ -123,15 +123,31 @@ export async function runMerchantWireTest(t: GlobalTestState) {
{ walletClient, exchange, merchant },
);
- await exchange.runWirewatchOnce();
- await exchange.runTransferOnce();
+ await exchange.runAggregatorOnce();
await merchant.runDepositcheckOnce();
-
- const resp = succeedOrThrow(
- await merchantClient.listIncomingWireTransfers(merchantAdminAccessToken),
- );
-
- console.log(j2s(resp));
+ {
+ const resp = succeedOrThrow(
+ await merchantClient.listIncomingWireTransfers(merchantAdminAccessToken),
+ );
+ t.assertDeepEqual(resp.incoming.length, 1);
+ // No reconciliation happened yet.
+ t.assertTrue(resp.incoming[0].expected_credit_amount == null);
+ console.log(j2s(resp));
+ }
+
+ await merchant.runReconciliationOnce();
+
+ {
+ const resp = succeedOrThrow(
+ await merchantClient.listIncomingWireTransfers(merchantAdminAccessToken),
+ );
+ console.log(j2s(resp));
+ t.assertDeepEqual(resp.incoming.length, 1);
+ t.assertAmountEquals(
+ resp.incoming[0].expected_credit_amount!,
+ "TESTKUDOS:4.79",
+ );
+ }
}
runMerchantWireTest.suites = ["wallet"];
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -44,6 +44,7 @@ import {
codecForChallengeRequestResponse,
codecForChallengeResponse,
codecForClaimResponse,
+ codecForIncomingTansferList,
codecForInstancesResponse,
codecForInventorySummaryResponse,
codecForLoginTokenSuccessResponse,
@@ -1834,7 +1835,7 @@ export class TalerMerchantInstanceHttpClient {
switch (resp.status) {
case HttpStatusCode.Ok:
- return opSuccessFromHttp(resp, codecForTansferList());
+ return opSuccessFromHttp(resp, codecForIncomingTansferList());
case HttpStatusCode.Unauthorized: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound: // FIXME: missing in docs
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -1482,7 +1482,7 @@ export interface ListConfirmedWireTransferRequestParams {
*/
limit?: number;
/**
- *
+ *
*/
order?: "asc" | "dec";
/**
@@ -1515,7 +1515,7 @@ export interface ListIncomingWireTransferRequestParams {
*/
limit?: number;
/**
- *
+ *
*/
order?: "asc" | "dec";
/**
@@ -2981,6 +2981,11 @@ export interface TransferList {
// List of all the transfers that fit the filter that we know.
transfers: TransferDetails[];
}
+
+export interface IncomingTransferList {
+ incoming: IncomingTransferDetails[];
+}
+
export interface TransferDetails {
// How much was wired to the merchant (minus fees).
credit_amount: AmountString;
@@ -3013,6 +3018,52 @@ export interface TransferDetails {
confirmed?: boolean;
}
+export interface IncomingTransferDetails {
+ // How much was wired to the merchant (minus fees).
+ expected_credit_amount?: AmountString;
+
+ // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
+ wtid: WireTransferIdentifierRawP;
+
+ // Target account that received the wire transfer.
+ payto_uri: PaytoString;
+
+ // Base URL of the exchange that made the wire transfer.
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filtering via offset.
+ expected_transfer_serial_id?: number;
+
+ // Time of the execution of the wire transfer by the exchange, according to the exchange
+ // Only provided if we did get an answer from the exchange.
+ execution_time?: Timestamp;
+
+ // True if we checked the exchange's answer and are happy with
+ // the reconciation data.
+ // False if we have an answer and are unhappy, missing if we
+ // do not have an answer from the exchange.
+ // Does not imply that the wire transfer was settled (for
+ // that, see confirmed).
+ validated: boolean;
+
+ // True if the merchant uses the POST /transfers API to confirm
+ // that this wire transfer took place (and it is thus not
+ // something merely claimed by the exchange).
+ // (a matching entry exists in /private/transfers)
+ confirmed: boolean;
+
+ // Last HTTP status we received from the exchange, 0 for
+ // none (incl. timeout)
+ last_http_status: Integer;
+
+ // Last Taler error code we got from the exchange.
+ last_ec: number;
+
+ // Last error detail we got back from the exchange.
+ last_error_detail?: string;
+}
+
export interface OtpDeviceAddDetails {
// Device ID to use.
otp_device_id: string;
@@ -3445,35 +3496,30 @@ export enum StatisticBucketRange {
Year = "year",
}
-
export interface StatisticAmountByBucket {
+ // Start time of the bucket (inclusive)
+ start_time: Timestamp;
- // Start time of the bucket (inclusive)
- start_time: Timestamp;
-
- // End time of the bucket (exclusive)
- end_time: Timestamp;
-
- // Range of the bucket
- range: StatisticBucketRange;
+ // End time of the bucket (exclusive)
+ end_time: Timestamp;
- // Sum of all amounts falling under the given
- // SLUG within this timeframe.
- cumulative_amounts: AmountString[];
+ // Range of the bucket
+ range: StatisticBucketRange;
+ // Sum of all amounts falling under the given
+ // SLUG within this timeframe.
+ cumulative_amounts: AmountString[];
}
export interface StatisticAmountByInterval {
+ // Start time of the interval.
+ // The interval always ends at the response
+ // generation time.
+ start_time: Timestamp;
- // Start time of the interval.
- // The interval always ends at the response
- // generation time.
- start_time: Timestamp;
-
- // Sum of all amounts falling under the given
- // SLUG within this timeframe.
- cumulative_amounts: AmountString[];
-
+ // Sum of all amounts falling under the given
+ // SLUG within this timeframe.
+ cumulative_amounts: AmountString[];
}
export interface StatisticsAmount {
@@ -3493,7 +3539,6 @@ export interface StatisticsAmount {
}
export interface StatisticCounterByBucket {
-
// Start time of the bucket (inclusive)
start_time: Timestamp;
@@ -3506,11 +3551,9 @@ export interface StatisticCounterByBucket {
// Sum of all counters falling under the given
// SLUG within this timeframe.
cumulative_counter: number;
-
}
export interface StatisticCounterByInterval {
-
// Start time of the interval.
// The interval always ends at the response
// generation time.
@@ -3519,11 +3562,8 @@ export interface StatisticCounterByInterval {
// Sum of all counters falling under the given
// SLUG within this timeframe.
cumulative_counter: number;
-
}
-
-
export interface StatisticsCounter {
// Statistics kept for a particular fixed time window.
buckets: StatisticCounterByBucket[];
@@ -4621,6 +4661,11 @@ export const codecForTansferList = (): Codec<TransferList> =>
.property("transfers", codecForList(codecForTransferDetails()))
.build("TalerMerchantApi.TransferList");
+export const codecForIncomingTansferList = (): Codec<IncomingTransferList> =>
+ buildCodecForObject<IncomingTransferList>()
+ .property("incoming", codecForList(codecForIncomingTransferDetails()))
+ .build("TalerMerchantApi.IncomingTransferList");
+
export const codecForTransferDetails = (): Codec<TransferDetails> =>
buildCodecForObject<TransferDetails>()
.property("credit_amount", codecForAmountString())
@@ -4633,6 +4678,22 @@ export const codecForTransferDetails = (): Codec<TransferDetails> =>
.property("confirmed", codecOptional(codecForBoolean()))
.build("TalerMerchantApi.TransferDetails");
+export const codecForIncomingTransferDetails =
+ (): Codec<IncomingTransferDetails> =>
+ buildCodecForObject<IncomingTransferDetails>()
+ .property("expected_credit_amount", codecOptional(codecForAmountString()))
+ .property("expected_transfer_serial_id", codecOptional(codecForNumber()))
+ .property("execution_time", codecOptional(codecForTimestamp))
+ .property("wtid", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("exchange_url", codecForURLString())
+ .property("validated", codecForBoolean())
+ .property("confirmed", codecForBoolean())
+ .property("last_http_status", codecForNumber())
+ .property("last_ec", codecForNumber())
+ .property("last_error_detail", codecOptional(codecForAny()))
+ .build("TalerMerchantApi.IncomingTransferDetails");
+
export const codecForOtpDeviceSummaryResponse =
(): Codec<OtpDeviceSummaryResponse> =>
buildCodecForObject<OtpDeviceSummaryResponse>()
@@ -4773,20 +4834,21 @@ export const codecForStatisticBucketRange = codecForEither(
codecForConstString(StatisticBucketRange.Year),
);
-
-export const codecForStatisticsAmountBucket = (): Codec<StatisticAmountByBucket> =>
- buildCodecForObject<StatisticAmountByBucket>()
- .property("start_time", codecForTimestamp)
- .property("end_time", codecForTimestamp)
- .property("range", codecForStatisticBucketRange) // FIXME Bucket range string to be specific
- .property("cumulative_amounts", codecForList(codecForAmountString()))
- .build("TalerMerchantApi.StatisticsAmountBucket");
-
-export const codecForStatisticsAmountInterval = (): Codec<StatisticAmountByInterval> =>
- buildCodecForObject<StatisticAmountByInterval>()
- .property("start_time", codecForTimestamp)
- .property("cumulative_amounts", codecForList(codecForAmountString()))
- .build("TalerMerchantApi.StatisticsAmountInterval");
+export const codecForStatisticsAmountBucket =
+ (): Codec<StatisticAmountByBucket> =>
+ buildCodecForObject<StatisticAmountByBucket>()
+ .property("start_time", codecForTimestamp)
+ .property("end_time", codecForTimestamp)
+ .property("range", codecForStatisticBucketRange) // FIXME Bucket range string to be specific
+ .property("cumulative_amounts", codecForList(codecForAmountString()))
+ .build("TalerMerchantApi.StatisticsAmountBucket");
+
+export const codecForStatisticsAmountInterval =
+ (): Codec<StatisticAmountByInterval> =>
+ buildCodecForObject<StatisticAmountByInterval>()
+ .property("start_time", codecForTimestamp)
+ .property("cumulative_amounts", codecForList(codecForAmountString()))
+ .build("TalerMerchantApi.StatisticsAmountInterval");
export const codecForStatisticsAmountResponse = (): Codec<StatisticsAmount> =>
buildCodecForObject<StatisticsAmount>()
@@ -4796,20 +4858,21 @@ export const codecForStatisticsAmountResponse = (): Codec<StatisticsAmount> =>
.property("intervals_description", codecOptional(codecForString()))
.build("TalerMerchantApi.StatisticsAmountResponse");
-export const codecForStatisticsCounterBucket = (): Codec<StatisticCounterByBucket> =>
- buildCodecForObject<StatisticCounterByBucket>()
- .property("start_time", codecForTimestamp)
- .property("end_time", codecForTimestamp)
- .property("range", codecForString()) // FIXME Bucket range string to be specific
- .property("cumulative_counter", codecForNumber())
- .build("TalerMerchantApi.StatisticsCounterBucket");
-
-export const codecForStatisticsCounterInterval = (): Codec<StatisticCounterByInterval> =>
- buildCodecForObject<StatisticCounterByInterval>()
- .property("start_time", codecForTimestamp)
- .property("cumulative_counter", codecForNumber())
- .build("TalerMerchantApi.StatisticsCounterInterval");
-
+export const codecForStatisticsCounterBucket =
+ (): Codec<StatisticCounterByBucket> =>
+ buildCodecForObject<StatisticCounterByBucket>()
+ .property("start_time", codecForTimestamp)
+ .property("end_time", codecForTimestamp)
+ .property("range", codecForString()) // FIXME Bucket range string to be specific
+ .property("cumulative_counter", codecForNumber())
+ .build("TalerMerchantApi.StatisticsCounterBucket");
+
+export const codecForStatisticsCounterInterval =
+ (): Codec<StatisticCounterByInterval> =>
+ buildCodecForObject<StatisticCounterByInterval>()
+ .property("start_time", codecForTimestamp)
+ .property("cumulative_counter", codecForNumber())
+ .build("TalerMerchantApi.StatisticsCounterInterval");
export const codecForStatisticsCounterResponse = (): Codec<StatisticsCounter> =>
buildCodecForObject<StatisticsCounter>()