commit 0ee19f2c03e0366cb73077e325f97f5b823fa2ae
parent d19eb71c6c4f25929dd219b4450b1829cddd896f
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 15 Jan 2026 14:49:36 -0300
fix #10672
Diffstat:
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, """)
- .replace(/'/g, "'")
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">");
- 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),