taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-harness/src/harness/harness.ts | 9+++++++++
Mpackages/taler-harness/src/integrationtests/test-merchant-wire.ts | 32++++++++++++++++++++++++--------
Mpackages/taler-util/src/http-client/merchant.ts | 3++-
Mpackages/taler-util/src/types-taler-merchant.ts | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
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>()