taler-typescript-core

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

commit 0ee19f2c03e0366cb73077e325f97f5b823fa2ae
parent d19eb71c6c4f25929dd219b4450b1829cddd896f
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Thu, 15 Jan 2026 14:49:36 -0300

fix #10672

Diffstat:
Mpackages/aml-backoffice-ui/src/pages/AccountList.tsx | 91++++++++++++++++++-------------------------------------------------------------
Mpackages/taler-util/src/http-client/exchange-client.ts | 98++++++++++++++++++++++++++++++++++---------------------------------------------
2 files changed, 62 insertions(+), 127 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/pages/AccountList.tsx b/packages/aml-backoffice-ui/src/pages/AccountList.tsx @@ -29,6 +29,7 @@ import { ErrorLoading, InputToggle, Loading, + LocalNotificationBanner, Pagination, RouteDefinition, useExchangeApiContext, @@ -45,6 +46,8 @@ import xlsIcon from "../assets/excel-icon.png"; import { useOfficer } from "../hooks/officer.js"; import { Profile } from "./Profile.js"; +const utfDecoder = new TextDecoder("utf-8"); + export function AccountList({ routeToAccountById: caseByIdRoute, }: { @@ -56,6 +59,9 @@ export function AccountList({ const [highRisk, setHighRisk] = useState<boolean>(); const list = useAmlAccounts({ investigated, open: opened, highRisk }); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const officer = useOfficer(); + const session = officer.state === "ready" ? officer.account : undefined; + const { lib } = useExchangeApiContext(); if (!list) { return <Loading />; @@ -162,23 +168,24 @@ export function AccountList({ : `not-investigated`; const time = format(new Date(), "yyyyMMdd_HHmmss"); - const downloadCsv = safeFunctionHandler(convertToCsv, [fields, records]); - const downloadXls = safeFunctionHandler(convertToXls, [fields, records]); + const downloadCsv = safeFunctionHandler(lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), session ? [session, "text/csv"] : undefined); + const downloadXls = safeFunctionHandler(lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), session ? [session, "application/vnd.ms-excel"] : undefined); downloadCsv.onSuccess = (result) => { setExported({ - content: result, + content: utfDecoder.decode(result), file: `accounts_${time}_${fileDescription}.csv`, }); }; downloadXls.onSuccess = (result) => { setExported({ - content: result, + content: utfDecoder.decode(result), file: `accounts_${time}_${fileDescription}.xls`, }); }; return ( <div> + <LocalNotificationBanner notification={notification} /> <div class="sm:flex sm:items-center"> <div class="px-2 sm:flex-auto"> <h1 class="text-base font-semibold leading-6 text-gray-900"> @@ -281,23 +288,23 @@ export function AccountList({ </tr> </thead> <tbody class="divide-y divide-gray-200 bg-white"> - {records.map((r,i) => { + {records.map((r, i) => { const uri = Paytos.asString(r.full_payto); if (i === 1) { r.open_time = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()) - r.close_time = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({minutes:5}))) + r.close_time = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ minutes: 5 }))) } else if (i === 2) { r.open_time = AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now()) } - const openTime = r.open_time.t_s !== "never" ? format(r.open_time.t_s*1000, "yyyy/MM/dd HH:mm") : undefined; - const closeTime = r.close_time.t_s !== "never" ? format(r.close_time.t_s*1000, "yyyy/MM/dd HH:mm") : undefined; + const openTime = r.open_time.t_s !== "never" ? format(r.open_time.t_s * 1000, "yyyy/MM/dd HH:mm") : undefined; + const closeTime = r.close_time.t_s !== "never" ? format(r.close_time.t_s * 1000, "yyyy/MM/dd HH:mm") : undefined; const openDescription = openTime ? closeTime - ? <span><i18n.Translate>From {openTime}<br/>To {closeTime}</i18n.Translate></span> + ? <span><i18n.Translate>From {openTime}<br />To {closeTime}</i18n.Translate></span> : i18n.str`Since ${openTime}` : i18n.str`Not opened`; - const paramsDesc = Object.entries(uri.params).map(([name,value]) => { + const paramsDesc = Object.entries(uri.params).map(([name, value]) => { return <div>{name}: {value}</div> }) return ( @@ -342,9 +349,9 @@ export function AccountList({ </span> ) : undefined} {r.open_time.t_s !== "never" ? ( - <span title={i18n.str`With custom rules`}> - <OpenIcon stroke="#1806beff" stroke-width="2" /> - </span> + <span title={i18n.str`With custom rules`}> + <OpenIcon stroke="#1806beff" stroke-width="2" /> + </span> ) : undefined} </td> </tr> @@ -676,61 +683,3 @@ const fields: FieldSet<CustomerAccountSummary> = [ type FieldSet<T> = Field<T>[]; type Field<T> = { name: string; type: string; convert: (o: T) => string }; -const CSV_HEADER = ""; -const CSV_FOOTER = "\r\n"; - -const XML_HEADER = - '<?xml version="1.0"?>' + - '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"' + - 'xmlns:o="urn:schemas-microsoft-com:office:office"' + - 'xmlns:x="urn:schemas-microsoft-com:office:excel"' + - 'xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">' + - '<Worksheet ss:Name="Sheet1">' + - "<Table>"; -const XML_FOOTER = "</Table></Worksheet></Workbook>"; - -async function convertToCsv( - fields: FieldSet<CustomerAccountSummary>, - values: CustomerAccountSummary[], -) { - const columns = fields.map((f) => f.name).join(","); - const HEADER = `${columns}\r\n`; - - const rows = values.reduce((prev, v) => { - const cells = fields - .map((f) => { - const str = f.convert(v); - return str.replace(/"/g, '\\"'); - }) - .join(","); - return `${cells}\r\n`; - }, ""); - const result = CSV_HEADER + HEADER + rows + CSV_FOOTER; - return opFixedSuccess(result); -} - -async function convertToXls( - fields: FieldSet<CustomerAccountSummary>, - values: CustomerAccountSummary[], -) { - const columns = fields.reduce((prev, f) => { - return `${prev}<Cell ss:StyleID="Header"><Data ss:Type=\"String\">${f.name}</Data></Cell>`; - }, ""); - const HEADER = `<Row>${columns}</Row>`; - - const rows = values.reduce((prev, v) => { - const cells = fields.reduce((prev, f) => { - const str = f.convert(v); - const safe = str - .replace(/"/g, "&quot;") - .replace(/'/g, "&apos;") - .replace(/&/g, "&amp;") - .replace(/</g, "&lt;") - .replace(/>/g, "&gt;"); - return `${prev}<Cell><Data ss:Type=\"${f.type}\">${safe}</Data></Cell>`; - }, ""); - return `${prev}<Row>${cells}</Row>`; - }, ""); - const result = XML_HEADER + HEADER + rows + XML_FOOTER; - return opFixedSuccess(result); -} diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -945,7 +945,7 @@ export class TalerExchangeHttpClient { } /** - * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-accounts * */ async getAmlAccounts( @@ -995,6 +995,40 @@ export class TalerExchangeHttpClient { } /** + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-accounts + * + */ + async getAmlAccountsAsOtherFormat( + auth: OfficerAccount, + mime: "text/csv" | "application/vnd.ms-excel" | "application/json", + ) { + const url = new URL(`aml/${auth.id}/accounts`, this.baseUrl); + + const resp = await this.fetch(url, { + headers: { + Accept: mime, + "Taler-AML-Officer-Signature": encodeCrock( + signAmlQuery(auth.signingKey), + ), + }, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + return opFixedSuccess(await resp.bytes()); + } + case HttpStatusCode.NoContent: + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + + /** * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions * */ @@ -1092,7 +1126,7 @@ export class TalerExchangeHttpClient { } /** - * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_NORMALIZED_PAYTO * */ async getAmlAttributesForAccount( @@ -1132,19 +1166,15 @@ export class TalerExchangeHttpClient { } } + /** + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_NORMALIZED_PAYTO + * + */ async getAmlAttributesForAccountAsPdf( auth: OfficerAccount, account: string, params: PaginationParams = {}, - ): Promise< - | OperationOk<ArrayBuffer> - | OperationFail< - | HttpStatusCode.Forbidden - | HttpStatusCode.NotFound - | HttpStatusCode.Conflict - | HttpStatusCode.NoContent - > - > { + ) { const url = new URL(`aml/${auth.id}/attributes/${account}`, this.baseUrl); addPaginationParams(url, params); @@ -1171,43 +1201,6 @@ export class TalerExchangeHttpClient { } } - async getAmlAccountsAsOtherFormat( - auth: OfficerAccount, - mime: string, - ): Promise< - | OperationOk<ArrayBuffer> - | OperationFail< - | HttpStatusCode.Forbidden - | HttpStatusCode.NotFound - | HttpStatusCode.Conflict - | HttpStatusCode.NoContent - > - > { - const url = new URL(`aml/${auth.id}/accounts`, this.baseUrl); - - const resp = await this.fetch(url, { - headers: { - Accept: mime, - "Taler-AML-Officer-Signature": encodeCrock( - signAmlQuery(auth.signingKey), - ), - }, - }); - - switch (resp.status) { - case HttpStatusCode.Ok: { - return opFixedSuccess(await resp.bytes()); - } - case HttpStatusCode.NoContent: - case HttpStatusCode.Forbidden: - case HttpStatusCode.NotFound: - case HttpStatusCode.Conflict: - return opKnownHttpFailure(resp.status, resp); - default: - return opUnknownHttpFailure(resp); - } - } - /** * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision * @@ -1215,14 +1208,7 @@ export class TalerExchangeHttpClient { async makeAmlDesicion( auth: OfficerAccount, decision: Omit<AmlDecisionRequest, "officer_sig">, - ): Promise< - | OperationOk<void> - | OperationFail< - | HttpStatusCode.Forbidden - | HttpStatusCode.NotFound - | HttpStatusCode.Conflict - > - > { + ) { const body: AmlDecisionRequest = { officer_sig: encodeCrock( signAmlDecision(auth.signingKey, decision),