commit 1e3c9b85b64dd4367a1ed9272b1ff6b0f63090f3 parent 36951014eea03f0f9da872b62a8a48e9cc55d87d Author: Sebastian <sebasjm@taler-systems.com> Date: Wed, 7 Jan 2026 08:59:01 -0300 updating spec for #10839 Diffstat:
30 files changed, 636 insertions(+), 267 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx @@ -26,17 +26,17 @@ import { InputSearchOnList } from "../form/InputSearchOnList.js"; const TALER_SCREEN_ID = 21; type Form = { - product: TalerMerchantApi.ProductDetail & WithId; + product: TalerMerchantApi.ProductDetailResponse & WithId; quantity: number; }; export interface Props { currentProducts: ProductMap; onAddProduct: ( - product: TalerMerchantApi.ProductDetail & WithId, + product: TalerMerchantApi.ProductDetailResponse & WithId, quantity: number, ) => void; - inventory: (TalerMerchantApi.ProductDetail & WithId)[]; + inventory: (TalerMerchantApi.ProductDetailResponse & WithId)[]; } export function InventoryProductForm({ diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -33,7 +33,7 @@ import { UIElement } from "../../hooks/preference.js"; const TALER_SCREEN_ID = 22; -type Entity = TalerMerchantApi.Product; +type Entity = TalerMerchantApi.ProductSold; interface Props { onAddProduct: (p: Entity) => Promise<void>; @@ -52,7 +52,7 @@ export function NonInventoryProductFrom({ }, [isEditing]); const [submitForm, addFormSubmitter] = useListener< - Partial<TalerMerchantApi.Product> | undefined + Partial<TalerMerchantApi.ProductSold> | undefined >((result) => { if (result) { setShowCreateProduct(false); @@ -176,7 +176,7 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { }); const submit = useCallback((): Entity | undefined => { - return value as TalerMerchantApi.Product; + return value as TalerMerchantApi.ProductSold; }, [value]); const hasErrors = errors !== undefined; diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -46,7 +46,7 @@ import { UIElement } from "../../hooks/preference.js"; const TALER_SCREEN_ID = 23; -type Entity = TalerMerchantApi.ProductDetail & { +type Entity = TalerMerchantApi.ProductDetailResponse & { product_id: string; }; @@ -159,7 +159,7 @@ export function ProductForm({ delete result.minimum_age; } - return result as TalerMerchantApi.ProductDetail & { + return result as TalerMerchantApi.ProductDetailResponse & { product_id: string; }; }, [value]); diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx @@ -22,11 +22,11 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; const TALER_SCREEN_ID = 24; interface Props { - list: TalerMerchantApi.Product[]; + list: TalerMerchantApi.ProductSold[]; actions?: { name: string; tooltip: string; - handler: (d: TalerMerchantApi.Product, index: number) => void; + handler: (d: TalerMerchantApi.ProductSold, index: number) => void; }[]; } export function ProductList({ list, actions = [] }: Props): VNode { diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -239,6 +239,7 @@ function getLongPollingReason( case MerchantAccountKycStatus.EXCHANGE_GATEWAY_TIMEOUT: case MerchantAccountKycStatus.EXCHANGE_UNREACHABLE: case MerchantAccountKycStatus.EXCHANGE_STATUS_INVALID: + case MerchantAccountKycStatus.MERCHANT_INTERNAL_ERROR: return undefined; case MerchantAccountKycStatus.KYC_WIRE_REQUIRED: return KycStatusLongPollingReason.AUTH_TRANSFER; diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts @@ -46,7 +46,7 @@ describe("product api interaction with listing", () => { }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetailResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( @@ -75,7 +75,7 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_CREATE_PRODUCT, { request: { price: "ARS:23", - } as TalerMerchantApi.ProductAddDetail, + } as TalerMerchantApi.ProductAddDetailRequest, }); env.addRequestExpectation(API_LIST_PRODUCTS, { @@ -86,17 +86,17 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as TalerMerchantApi.ProductDetail, + } as TalerMerchantApi.ProductDetailResponse, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as TalerMerchantApi.ProductDetail, + } as TalerMerchantApi.ProductDetailResponse, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { response: { price: "ARS:23", - } as TalerMerchantApi.ProductDetail, + } as TalerMerchantApi.ProductDetailResponse, }); api.instance.addProduct(undefined, { @@ -144,7 +144,7 @@ describe("product api interaction with listing", () => { }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetailResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( @@ -173,7 +173,7 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { request: { price: "ARS:13", - } as TalerMerchantApi.ProductPatchDetail, + } as TalerMerchantApi.ProductAddDetailRequest, }); env.addRequestExpectation(API_LIST_PRODUCTS, { @@ -184,7 +184,7 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:13", - } as TalerMerchantApi.ProductDetail, + } as TalerMerchantApi.ProductDetailResponse, }); api.instance.updateProduct(undefined, "1234", { @@ -222,10 +222,10 @@ describe("product api interaction with listing", () => { }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetailResponse, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" } as TalerMerchantApi.ProductDetail, + response: { price: "ARS:23" } as TalerMerchantApi.ProductDetailResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( @@ -265,7 +265,7 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as TalerMerchantApi.ProductDetail, + } as TalerMerchantApi.ProductDetailResponse, }); api.instance.deleteProduct(undefined, "2345"); }, @@ -300,7 +300,7 @@ describe("product api interaction with details", () => { env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { response: { description: "this is a description", - } as TalerMerchantApi.ProductDetail, + } as TalerMerchantApi.ProductDetailResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( @@ -328,13 +328,13 @@ describe("product api interaction with details", () => { env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { request: { description: "other description", - } as TalerMerchantApi.ProductPatchDetail, + } as TalerMerchantApi.ProductAddDetailRequest, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { response: { description: "other description", - } as TalerMerchantApi.ProductDetail, + } as TalerMerchantApi.ProductDetailResponse, }); api.instance.updateProduct(undefined, "12", { diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -31,7 +31,7 @@ import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; -export type ProductWithId = TalerMerchantApi.ProductDetail & { +export type ProductWithId = TalerMerchantApi.ProductDetailResponse & { id: string; serial: number; }; diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts @@ -92,7 +92,7 @@ export const API_INFORM_TRANSFERS: Query< //////////////////// export const API_CREATE_PRODUCT: Query< - TalerMerchantApi.ProductAddDetail, + TalerMerchantApi.ProductAddDetailRequest, unknown > = { method: "POST", @@ -109,7 +109,7 @@ export const API_LIST_PRODUCTS: Query< export const API_GET_PRODUCT_BY_ID = ( id: string, -): Query<unknown, TalerMerchantApi.ProductDetail> => ({ +): Query<unknown, TalerMerchantApi.ProductDetailResponse> => ({ method: "GET", url: `http://backend/instances/default/private/products/${id}`, }); @@ -117,7 +117,7 @@ export const API_GET_PRODUCT_BY_ID = ( export const API_UPDATE_PRODUCT_BY_ID = ( id: string, ): Query< - TalerMerchantApi.ProductPatchDetail, + TalerMerchantApi.ProductPatchDetailRequest, TalerMerchantApi.InventorySummaryResponse > => ({ method: "PATCH", diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx @@ -55,6 +55,7 @@ function createExample<Props>( num_fractional_trailing_zero_digits: 1, }, }, + report_generators: {}, default_wire_transfer_rounding_interval: RoundingInterval.NONE, default_pay_delay: { d_us: "forever" }, have_donau: false, diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx @@ -55,6 +55,7 @@ function createExample<Props>( num_fractional_trailing_zero_digits: 1, }, }, + report_generators: {}, default_wire_transfer_rounding_interval: RoundingInterval.NONE, default_pay_delay: { d_us: "forever" }, have_donau: false, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -45,7 +45,6 @@ interface Props { export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { const { i18n } = useTranslationContext(); - const { state, lib } = useSessionContext(); const result = useInstanceBankAccounts(); if (!result) return <Loading />; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx @@ -175,7 +175,7 @@ function ProductListSmall({ onSelect, }: { onSelect: () => void; - list: TalerMerchantApi.ProductSummary[]; + list: TalerMerchantApi.CategoryProductSummary[]; }): VNode { const { i18n } = useTranslationContext(); const result = useInstanceProductsFromIds(list.map((d) => d.product_id)); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -160,40 +160,46 @@ function PendingTable({ case TalerMerchantApi.MerchantAccountKycStatus .AWAITING_AML_REVIEW: return ( - <i18n.Translate>Awaiting for account review.</i18n.Translate> + <i18n.Translate> + Awaiting for account review. + </i18n.Translate> ); case TalerMerchantApi.MerchantAccountKycStatus.READY: return <i18n.Translate>Ready</i18n.Translate>; case TalerMerchantApi.MerchantAccountKycStatus .NO_EXCHANGE_KEY: + return <i18n.Translate>Syncing...</i18n.Translate>; + case TalerMerchantApi.MerchantAccountKycStatus + .EXCHANGE_INTERNAL_ERROR: return ( <i18n.Translate> - Syncing... + Payment service internal error. Contact + administrator or check again later. </i18n.Translate> ); case TalerMerchantApi.MerchantAccountKycStatus - .EXCHANGE_INTERNAL_ERROR: + .MERCHANT_INTERNAL_ERROR: return ( <i18n.Translate> - Payment service internal error. Contact administrator or - check again later. + Merchant backend internal error. Contact + administrator or check again later. </i18n.Translate> ); case TalerMerchantApi.MerchantAccountKycStatus .EXCHANGE_GATEWAY_TIMEOUT: return ( <i18n.Translate> - Payment service timeout. Contact administrator or check - again later. + Payment service timeout. Contact administrator or + check again later. </i18n.Translate> ); case TalerMerchantApi.MerchantAccountKycStatus .EXCHANGE_UNREACHABLE: return ( <i18n.Translate> - Payment service unreachable. Contact administrator or check - again later. + Payment service unreachable. Contact administrator + or check again later. </i18n.Translate> ); case TalerMerchantApi.MerchantAccountKycStatus @@ -215,8 +221,8 @@ function PendingTable({ .EXCHANGE_STATUS_INVALID: return ( <i18n.Translate> - Payment service response is invalid. Contact administrator - or check again later. + Payment service response is invalid. Contact + administrator or check again later. </i18n.Translate> ); @@ -227,66 +233,6 @@ function PendingTable({ </td> </tr> ); - // if (e.payto_kycauths === undefined) { - // const spa = new URL(`kyc-spa/${e.access_token}`, e.exchange_url) - // .href; - // } else { - // const accounts = e.payto_kycauths; - // return ( - // <tr key={i}> - // <td onClick={() => onShowInstructions(accounts, e.payto_uri)}> - // {e.exchange_url} - // </td> - // <td - // onClick={() => onShowInstructions(accounts, e.payto_uri)} - // style={{ cursor: "pointer" }} - // > - // {e.payto_uri} - // </td> - // <td onClick={() => onShowInstructions(accounts, e.payto_uri)}> - // {e.status} - // <i18n.Translate> - // The Payment Service Provider requires an account - // verification. - // </i18n.Translate> - // </td> - // </tr> - // ); - // } - })} - </tbody> - </table> - </div> - ); -} - -function TimedOutTable({ entries }: TimedOutTableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - <table class="table is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Exchange</i18n.Translate> - </th> - <th> - <i18n.Translate>Code</i18n.Translate> - </th> - <th> - <i18n.Translate>Http Status</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {entries.map((e, i) => { - return ( - <tr key={i}> - <td>{e.exchange_url}</td> - <td>{e.exchange_code}</td> - <td>{e.exchange_http_status}</td> - </tr> - ); })} </tbody> </table> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -296,6 +296,23 @@ function ShowInstructionForKycRedirect({ </ConfirmModal> ); } + case TalerMerchantApi.MerchantAccountKycStatus.MERCHANT_INTERNAL_ERROR: { + return ( + <ConfirmModal + label={i18n.str`Ok`} + description={i18n.str`Internal error`} + active + onCancel={onCancel} + > + <p style={{ paddingTop: 0 }}> + <i18n.Translate> + The merchant service provider detected an internal error, contact + the system administrator or check again later. + </i18n.Translate> + </p> + </ConfirmModal> + ); + } case TalerMerchantApi.MerchantAccountKycStatus.EXCHANGE_GATEWAY_TIMEOUT: { return ( <ConfirmModal diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -68,7 +68,7 @@ export interface Props { onCreated: (id: string) => void; onBack?: () => void; instanceConfig: InstanceConfig; - instanceInventory: (TalerMerchantApi.ProductDetail & WithId)[]; + instanceInventory: (TalerMerchantApi.ProductDetailResponse & WithId)[]; } interface InstanceConfig { use_stefan: boolean; @@ -108,7 +108,7 @@ function with_defaults( } interface ProductAndQuantity { - product: TalerMerchantApi.ProductDetail & WithId; + product: TalerMerchantApi.ProductDetailResponse & WithId; quantity: number; } export interface ProductMap { @@ -137,7 +137,7 @@ interface Payments extends TalerForm { } interface Entity extends TalerForm { inventoryProducts: ProductMap; - products: TalerMerchantApi.Product[]; + products: TalerMerchantApi.ProductSold[]; pricing: Partial<Pricing>; payments: Partial<Payments>; shipping: Partial<Shipping>; @@ -337,7 +337,7 @@ export function CreatePage({ } }; const addProductToTheInventoryList = ( - product: TalerMerchantApi.ProductDetail & WithId, + product: TalerMerchantApi.ProductDetailResponse & WithId, quantity: number, ) => { valueHandler((v) => { @@ -355,7 +355,7 @@ export function CreatePage({ }); }; - const addNewProduct = async (product: TalerMerchantApi.Product) => { + const addNewProduct = async (product: TalerMerchantApi.ProductSold) => { return valueHandler((v) => { const products = v.products ? [...v.products, product] : []; return { ...v, products }; @@ -371,7 +371,7 @@ export function CreatePage({ }; const [editingProduct, setEditingProduct] = useState< - TalerMerchantApi.Product | undefined + TalerMerchantApi.ProductSold | undefined >(undefined); const totalPriceInventory = inventoryList.reduce((prev, cur) => { @@ -841,7 +841,7 @@ export function CreatePage({ ); } -function asProduct(p: ProductAndQuantity): TalerMerchantApi.Product { +function asProduct(p: ProductAndQuantity): TalerMerchantApi.ProductSold { return { product_id: p.product.id, product_name: p.product.product_name, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx @@ -41,7 +41,7 @@ export interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { state: session, lib } = useSessionContext(); - const [form, setForm] = useState<TalerMerchantApi.ProductAddDetail>(); + const [form, setForm] = useState<TalerMerchantApi.ProductAddDetailRequest>(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const create = safeFunctionHandler( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx @@ -23,7 +23,7 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; import { CreatePage } from "./CreatePage.js"; -export type Entity = TalerMerchantApi.ProductAddDetail; +export type Entity = TalerMerchantApi.ProductAddDetailRequest; interface Props { onBack?: () => void; onConfirm: () => void; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -51,7 +51,7 @@ import { const TALER_SCREEN_ID = 56; -type Entity = TalerMerchantApi.ProductDetail & WithId; +type Entity = TalerMerchantApi.ProductDetailResponse & WithId; interface Props { instances: Entity[]; @@ -379,7 +379,7 @@ function Table({ interface FastProductUpdateFormProps { product: Entity; - onUpdate: (data: TalerMerchantApi.ProductPatchDetail) => Promise<void>; + onUpdate: (data: TalerMerchantApi.ProductPatchDetailRequest) => Promise<void>; onCancel: () => void; } interface FastProductUpdate extends TalerForm { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -53,7 +53,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { const result = useInstanceProducts(); const { state: session, lib } = useSessionContext(); const [deleting, setDeleting] = useState< - (TalerMerchantApi.ProductDetail & WithId) | null + (TalerMerchantApi.ProductDetailResponse & WithId) | null >(null); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -111,7 +111,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { onLoadMoreAfter={result.loadNext} onCreate={onCreate} onSelect={(product) => onSelect(product.id)} - onDelete={(prod: TalerMerchantApi.ProductDetail & WithId) => + onDelete={(prod: TalerMerchantApi.ProductDetailResponse & WithId) => setDeleting(prod) } /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx @@ -36,7 +36,7 @@ import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchan const TALER_SCREEN_ID = 57; -type Entity = TalerMerchantApi.ProductDetail & { product_id: string }; +type Entity = TalerMerchantApi.ProductDetailResponse & { product_id: string }; interface Props { onBack?: () => void; @@ -46,7 +46,7 @@ interface Props { export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { const { state: session, lib } = useSessionContext(); - const [form, setForm] = useState<TalerMerchantApi.ProductDetail>(); + const [form, setForm] = useState<TalerMerchantApi.ProductDetailResponse>(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const update = safeFunctionHandler( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -28,7 +28,7 @@ import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -export type Entity = TalerMerchantApi.ProductAddDetail; +export type Entity = TalerMerchantApi.ProductAddDetailRequest; interface Props { onBack?: () => void; onConfirm: () => void; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx @@ -55,11 +55,9 @@ export const Example = createExample(TestedComponent, { payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString, transfer_serial_id: 123123123, wtid: "!@KJELQKWEJ!L@K#!J@", - confirmed: true, execution_time: { t_s: new Date().getTime() / 1000, }, - verified: false, }, { exchange_url: "http://exchange.url/", @@ -67,11 +65,9 @@ export const Example = createExample(TestedComponent, { payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString, transfer_serial_id: 123123123, wtid: "!@KJELQKWEJ!L@K#!J@", - confirmed: true, execution_time: { t_s: new Date().getTime() / 1000, }, - verified: false, }, { exchange_url: "http://exchange.url/", @@ -79,11 +75,9 @@ export const Example = createExample(TestedComponent, { payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString, transfer_serial_id: 123123123, wtid: "!@KJELQKWEJ!L@K#!J@", - confirmed: true, execution_time: { t_s: new Date().getTime() / 1000, }, - verified: false, }, ], accounts: ["payto://x-taler-bank/bank/some_account"], diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -30,7 +30,7 @@ const TALER_SCREEN_ID = 71; export interface Props { transfers: TalerMerchantApi.TransferDetails[]; - incomings: TalerMerchantApi.IncomingTransferDetails[]; + incomings: TalerMerchantApi.ExpectedTransferDetails[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; onShowVerified: () => void; @@ -40,7 +40,7 @@ export interface Props { accounts: string[]; onChangePayTo: (p?: string) => void; payTo?: string; - onSelectedToConfirm: (wid: TalerMerchantApi.IncomingTransferDetails) => void; + onSelectedToConfirm: (wid: TalerMerchantApi.ExpectedTransferDetails) => void; } export function ListPage({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -32,10 +32,10 @@ import { const TALER_SCREEN_ID = 72; interface TablePropsIncoming { - transfers: (TalerMerchantApi.IncomingTransferDetails & WithId)[]; + transfers: (TalerMerchantApi.ExpectedTransferDetails & WithId)[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; - onSelectedToConfirm: (d: TalerMerchantApi.IncomingTransferDetails) => void; + onSelectedToConfirm: (d: TalerMerchantApi.ExpectedTransferDetails) => void; } export function CardTableIncoming({ @@ -98,10 +98,12 @@ export function CardTableIncoming({ <tr key={i.id} style={{ - cursor: !i.confirmed ? "pointer" : undefined + cursor: !i.confirmed ? "pointer" : undefined, }} onClick={ - !i.confirmed ? () => onSelectedToConfirm(i) : undefined + !i.confirmed + ? () => onSelectedToConfirm(i) + : undefined } > <td title={i.wtid}>{i.wtid.substring(0, 16)}...</td> @@ -192,15 +194,8 @@ export function CardTableVerified({ <i18n.Translate>Amount</i18n.Translate> </th> <th> - <i18n.Translate>Confirmed</i18n.Translate> - </th> - <th> - <i18n.Translate>Verified</i18n.Translate> - </th> - <th> <i18n.Translate>Executed on</i18n.Translate> </th> - {/* <th /> */} </tr> </thead> <tbody> @@ -209,8 +204,6 @@ export function CardTableVerified({ <tr key={i.id}> <td title={i.wtid}>{i.wtid.substring(0, 16)}...</td> <td>{i.credit_amount}</td> - <td>{i.confirmed ? i18n.str`yes` : i18n.str`no`}</td> - <td>{i.verified ? i18n.str`yes` : i18n.str`no`}</td> <td> {i.execution_time ? i.execution_time.t_s == "never" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -20,38 +20,37 @@ */ import { + ExpectedTransferDetails, HttpStatusCode, - IncomingTransferDetails, TalerError, TransferDetails, - TransferInformation, - assertUnreachable, + assertUnreachable } from "@gnu-taler/taler-util"; import { PaginatedResult, useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; +import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; +import { Amount } from "../../../../components/Amount.js"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; +import { ConfirmModal, Row } from "../../../../components/modal/index.js"; +import { useSessionContext } from "../../../../context/session.js"; import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; import { + datetimeFormatForSettings, + usePreference, +} from "../../../../hooks/preference.js"; +import { useInstanceConfirmedTransfers, useInstanceIncomingTransfers, } from "../../../../hooks/transfer.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; -import { ConfirmModal, Row } from "../../../../components/modal/index.js"; -import { Amount } from "../../../../components/Amount.js"; -import { format } from "date-fns"; -import { - datetimeFormatForSettings, - usePreference, -} from "../../../../hooks/preference.js"; -import { useSessionContext } from "../../../../context/session.js"; interface Props { // onCreate: () => void; @@ -77,7 +76,7 @@ export default function ListTransfer({}: Props): VNode { ? [] : instance.body.accounts.map((a) => a.payto_uri); const [form, setForm] = useState<Form>({ payto_uri: "", verified: false }); - const [selected, setSelected] = useState<IncomingTransferDetails>(); + const [selected, setSelected] = useState<ExpectedTransferDetails>(); const { i18n } = useTranslationContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -120,7 +119,7 @@ export default function ListTransfer({}: Props): VNode { // const isNonVerifiedTransfers = form.verified === false; // const isAllTransfers = form.verified === undefined; - let incoming: PaginatedResult<IncomingTransferDetails[]>; + let incoming: PaginatedResult<ExpectedTransferDetails[]>; { const result = useInstanceIncomingTransfers( { diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -44,7 +44,8 @@ import { codecForChallengeRequestResponse, codecForChallengeResponse, codecForClaimResponse, - codecForIncomingTansferList, + codecForFullInventoryDetailsResponse, + codecForExpectedTansferList as codecForExpectedTansferList, codecForInstancesResponse, codecForInventorySummaryResponse, codecForLoginTokenSuccessResponse, @@ -59,7 +60,7 @@ import { codecForPaymentDeniedLegallyResponse, codecForPaymentResponse, codecForPostOrderResponse, - codecForProductDetail, + codecForProductDetailResponse, codecForQueryInstancesResponse, codecForStatisticsAmountResponse, codecForStatisticsCounterResponse, @@ -84,6 +85,10 @@ import { opKnownHttpFailure, opKnownTalerFailure, opUnknownHttpFailure, + codecForMerchantStatisticsReportResponse, + codecForReportAddedResponse, + codecForReportDetailResponse, + codecForReportsSummaryResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -160,7 +165,7 @@ export enum TalerMerchantManagementCacheEviction { * Uses libtool's current:revision:age versioning. */ export class TalerMerchantInstanceHttpClient { - public static readonly PROTOCOL_VERSION = "24:0:1"; + public static readonly PROTOCOL_VERSION = "25:0:2"; readonly httpLib: HttpRequestLibrary; readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>; @@ -803,20 +808,12 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForAccountKycRedirects()); - case HttpStatusCode.Accepted: - return opSuccessFromHttp(resp, codecForAccountKycRedirects()); case HttpStatusCode.NoContent: return opEmptySuccess(); case HttpStatusCode.Unauthorized: // FIXME: missing in docs return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: // FIXME: missing in docs return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.BadGateway: - return opKnownAlternativeHttpFailure( - resp, - resp.status, - codecForAccountKycRedirects(), - ); case HttpStatusCode.ServiceUnavailable: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.GatewayTimeout: @@ -1183,7 +1180,7 @@ export class TalerMerchantInstanceHttpClient { */ async addProduct( token: AccessToken | undefined, - body: TalerMerchantApi.ProductAddDetail, + body: TalerMerchantApi.ProductAddDetailRequest, ) { const url = new URL(`private/products`, this.baseUrl); @@ -1221,7 +1218,7 @@ export class TalerMerchantInstanceHttpClient { async updateProduct( token: AccessToken | undefined, productId: string, - body: TalerMerchantApi.ProductPatchDetail, + body: TalerMerchantApi.ProductPatchDetailRequest, ) { const url = new URL(`private/products/${productId}`, this.baseUrl); @@ -1315,7 +1312,7 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForMerchantPosProductDetail()); + return opSuccessFromHttp(resp, codecForFullInventoryDetailsResponse()); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -1340,7 +1337,7 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForProductDetail()); + return opSuccessFromHttp(resp, codecForProductDetailResponse()); case HttpStatusCode.Unauthorized: // FIXME: missing in docs return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: @@ -1831,7 +1828,7 @@ export class TalerMerchantInstanceHttpClient { url.searchParams.set("verified", params.verified ? "YES" : "NO"); } if (params.confirmed !== undefined) { - url.searchParams.set("verified", params.confirmed ? "YES" : "NO"); + url.searchParams.set("confirmed", params.confirmed ? "YES" : "NO"); } addPaginationParams(url, params); @@ -1846,7 +1843,7 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForIncomingTansferList()); + return opSuccessFromHttp(resp, codecForExpectedTansferList()); case HttpStatusCode.Unauthorized: // FIXME: missing in docs return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: // FIXME: missing in docs @@ -2774,6 +2771,152 @@ export class TalerMerchantInstanceHttpClient { } /** + * https://docs.taler.net/core/api-merchant.html#post--reports-$REPORT_ID + */ + async generateReport( + id: string, + body: TalerMerchantApi.ReportGenerationRequest, + ) { + const url = new URL(`reports/${id}`, this.baseUrl); + + const headers: Record<string, string> = {}; + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: { + return opEmptySuccess(); + } + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-reports + */ + async createScheduledReport( + body: TalerMerchantApi.ReportAddRequest, + ) { + const url = new URL(`private/reports`, this.baseUrl); + + const headers: Record<string, string> = {}; + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForReportAddedResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-reports-$REPORT_ID + */ + async updateScheduledReport( + id: string, + body: TalerMerchantApi.ReportAddRequest, + ) { + const url = new URL(`private/reports/${id}`, this.baseUrl); + + const headers: Record<string, string> = {}; + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForReportAddedResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-reports + */ + async listScheduledReports( + params: PaginationParams = {}, + ) { + const url = new URL(`private/reports`, this.baseUrl); + addPaginationParams(url, params); + + const headers: Record<string, string> = {}; + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForReportsSummaryResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-reports-$REPORT_SERIAL + */ + async getScheduledReport(id: string) { + const url = new URL(`private/reports/${id}`, this.baseUrl); + + const headers: Record<string, string> = {}; + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForReportDetailResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-reports-$REPORT_SERIAL + */ + async deleteScheduledReport(serial: string) { + const url = new URL(`private/reports/${serial}`, this.baseUrl); + + const headers: Record<string, string> = {}; + const resp = await this.httpLib.fetch(url.href, { + method: "DELETE", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + /** * Get the auth api against the current instance * * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token @@ -3194,6 +3337,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp return opUnknownHttpFailure(resp); } } + /** * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-amount-$SLUG */ @@ -3231,4 +3375,48 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp return opUnknownHttpFailure(resp); } } + + /** + * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-report-$NAME + */ + async getStatisticsReport( + token: AccessToken | undefined, + name: "transactions" | "money-pots" | "taxes" | "sales-funnel", + params: TalerMerchantApi.GetStatisticsReportParams = {}, + ) { + const url = new URL(`private/statistics-report/${name}`, this.baseUrl); + + if (params.count !== undefined) { + url.searchParams.set("count", String(params.count)); + } + if (params.granularity) { + url.searchParams.set("granularity", params.granularity); + } + + const headers: Record<string, string> = {}; + if (token) { + headers.Authorization = makeBearerTokenAuthHeader(token); + } + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp( + resp, + codecForMerchantStatisticsReportResponse(), + ); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotImplemented: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } } diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -377,13 +377,6 @@ export interface AbortingCoin { // Public key of a coin for which the wallet is requesting an abort-related refund. coin_pub: EddsaPublicKeyString; - // The amount to be refunded (matches the original contribution) - // @deprecated since **v18**. - /** - * @deprecated - */ - contribution?: AmountString; - // URL of the exchange this coin was withdrawn from. exchange_url: string; } @@ -524,34 +517,34 @@ export interface Tax { tax: AmountString; } -export interface Product { - // merchant-internal identifier for the product. - product_id?: string; +// export interface Product { +// // merchant-internal identifier for the product. +// product_id?: string; - // Human-readable product description. - description: string; +// // Human-readable product description. +// description: string; - // Map from IETF BCP 47 language tags to localized descriptions - description_i18n?: InternationalizedString; +// // Map from IETF BCP 47 language tags to localized descriptions +// description_i18n?: InternationalizedString; - // The number of units of the product to deliver to the customer. - quantity?: Integer; +// // The number of units of the product to deliver to the customer. +// quantity?: Integer; - // The unit in which the product is measured (liters, kilograms, packages, etc.) - unit?: string; +// // The unit in which the product is measured (liters, kilograms, packages, etc.) +// unit?: string; - // The price of the product; this is the total price for quantity times unit of this product. - price?: AmountString; +// // The price of the product; this is the total price for quantity times unit of this product. +// price?: AmountString; - // An optional base64-encoded product image - image?: ImageDataUrl; +// // An optional base64-encoded product image +// image?: ImageDataUrl; - // a list of taxes paid by the merchant for this product. Can be empty. - taxes?: Tax[]; +// // a list of taxes paid by the merchant for this product. Can be empty. +// taxes?: Tax[]; - // time indicating when this product should be delivered - delivery_date?: TalerProtocolTimestamp; -} +// // time indicating when this product should be delivered +// delivery_date?: TalerProtocolTimestamp; +// } /** * Contract terms from a merchant. @@ -624,7 +617,7 @@ interface MerchantContractTermsCommon { exchanges: Exchange[]; // List of products that are part of the purchase (see Product). - products?: Product[]; + products?: ProductSold[]; // After this deadline has passed, no refunds will be accepted. refund_deadline: TalerProtocolTimestamp; @@ -689,6 +682,14 @@ interface MerchantContractTermsCommon { // It might also be set independent of any product, due to // legal requirements. minimum_age?: Integer; + + // Default money pot to use for this product, applies to the + // amount remaining that was not claimed by money pots of + // products or taxes. Not useful to wallets, only for + // merchant-internal accounting. If not given, the remaining + // account is simply not accounted for in any money pot. + // Since **v25**. + default_money_pot?: Integer; } export enum MerchantContractVersion { @@ -1014,9 +1015,10 @@ const codecForMerchantContractTermsCommon = .property("merchant", codecForMerchantInfo()) .property("merchant_pub", codecForString()) .property("exchanges", codecForList(codecForExchange())) - .property("products", codecOptional(codecForList(codecForProduct()))) + .property("products", codecOptional(codecForList(codecForProductSold()))) .property("extra", codecForAny()) .property("minimum_age", codecOptional(codecForNumber())) + .property("default_money_pot", codecOptional(codecForNumber())) .build("TalerMerchantApi.ContractTermsCommon"); export const codecForMerchantContractTermsV0 = @@ -1236,6 +1238,11 @@ export interface MerchantVersionResponse { // used with. currencies: { [currency: string]: CurrencySpecification }; + // Maps available report generator configuration section names + // to descriptions of the respective report generator. + // Since **v25**. + report_generators: { [section_name: string]: string }; + // Array of exchanges trusted by the merchant. // Since protocol **v6**. exchanges: ExchangeConfigInfo[]; @@ -1599,6 +1606,11 @@ export interface GetStatisticsRequestParams { by?: "INTERVAL" | "BUCKET" | undefined; } +export interface GetStatisticsReportParams { + granularity?: StatisticBucketRange; + count?: number; +} + export interface PayRequest { // The coins used to make the payment. coins: CoinPaySig[]; @@ -1837,23 +1849,8 @@ export interface InstanceConfigurationMessage { export interface InstanceAuthConfigurationMessage { // Type of authentication. - // "external": The mechant backend does not do - // any authentication checks. Instead an API - // gateway must do the authentication. - // "token": (deprecated) The merchant checks an auth token. - // See "token" for details. - // Since **v19**: APIs use login tokens retrieved from the /private/token - // endpoint. - // See "password" for details. method: MerchantAuthMethod; - // Deprecated: For method "token", this field is mandatory. - // The token MUST begin with the string "secret-token:". - // After the auth token has been set (with method "token"), - // the value must be provided in a "Authorization: Bearer $token" - // header. - // token?: AccessToken; - // Since **v19**: For method "token", this field is mandatory. // Authentication against the /private/token endpoint // is done using basic authentication with the configured password @@ -1889,11 +1886,6 @@ export interface LoginTokenRequest { // Optional token description description?: string; - - // Can this token be refreshed? - // Defaults to false. Deprecated since **v19**. - // Use ":refreshable" scope prefix instead. - // refreshable?: boolean; } export interface LoginTokenSuccessResponse { @@ -2101,6 +2093,7 @@ export enum MerchantAccountKycStatus { AWAITING_AML_REVIEW = "awaiting-aml-review", READY = "ready", LOGIC_BUG = "logic-bug", + MERCHANT_INTERNAL_ERROR = "merchant-internal-error", EXCHANGE_INTERNAL_ERROR = "exchange-internal-error", EXCHANGE_GATEWAY_TIMEOUT = "exchange-gateway-timeout", EXCHANGE_UNREACHABLE = "exchange-unreachable", @@ -2128,6 +2121,7 @@ export function getMerchantAccountKycStatusSimplified( case MerchantAccountKycStatus.KYC_REQUIRED: return MerchantAccountKycStatusSimplified.ACTION_REQUIRED; case MerchantAccountKycStatus.NO_EXCHANGE_KEY: + case MerchantAccountKycStatus.MERCHANT_INTERNAL_ERROR: case MerchantAccountKycStatus.EXCHANGE_INTERNAL_ERROR: case MerchantAccountKycStatus.EXCHANGE_GATEWAY_TIMEOUT: case MerchantAccountKycStatus.EXCHANGE_UNREACHABLE: @@ -2151,6 +2145,7 @@ export interface MerchantAccountKycRedirect { // + "awaiting-aml-review": account under review by payment provider // + "ready": everything is fine, account can be fully used // - "logic-bug": merchant backend logic bug + // o "merchant-internal-error": merchant had an internal error // o "exchange-internal-error": exchange had an internal error // o "exchange-gateway-timeout": network timeout at gateway // o "exchange-unreachable": exchange did not respond at all @@ -2375,10 +2370,10 @@ export interface CategoryProductList { name_i18n?: { [lang_tag: string]: string }; // The products in this category. - products: ProductSummary[]; + products: CategoryProductSummary[]; } -export interface ProductSummary { +export interface CategoryProductSummary { // Product ID to use. product_id: string; } @@ -2397,7 +2392,7 @@ export interface CategoryCreatedResponse { category_id: Integer; } -export interface ProductAddDetail { +export interface ProductAddDetailRequest { // Product ID to use. product_id: string; @@ -2448,9 +2443,20 @@ export interface ProductAddDetail { // Minimum age buyer must have (in years). Default is 0. minimum_age?: Integer; + + // Product group the product belongs to. 0 and missing both + // means default. + // Since **v25**. + product_group_id?: Integer; + + // Money pot revenue on the product should be accounted in. + // 0 and missing both mean no money pot (revenue accounted + // in money pot of the overall order or not at all). + // Since **v25**. + money_pot_id?: Integer; } -export interface ProductPatchDetail { +export interface ProductPatchDetailRequest { // Human-readable product name. // Since API version **v20**. Optional only for // backwards-compatibility, should be considered mandatory @@ -2501,6 +2507,12 @@ export interface ProductPatchDetail { // Minimum age buyer must have (in years). Default is 0. minimum_age?: Integer; + + // Money pot revenue on the product should be accounted in. + // 0 and missing both mean no money pot (revenue accounted + // in money pot of the overall order or not at all). + // Since **v25**. + money_pot_id?: Integer; } export interface InventorySummaryResponse { @@ -2581,7 +2593,7 @@ export interface MerchantCategory { name_i18n?: { [lang_tag: string]: string }; } -export interface ProductDetail { +export interface ProductDetailResponse { // Human-readable product name. // Since API version **v20**. product_name?: string; @@ -2632,6 +2644,16 @@ export interface ProductDetail { // Minimum age buyer must have (in years). minimum_age?: Integer; + + // Product group the product belongs to. Missing means default. + // Since **v25**. + product_group_id?: Integer; + + // Money pot revenue on the product should be accounted in. + // Missing means no money pot (revenue accounted + // in money pot of the overall order or not at all). + // Since **v25**. + money_pot_id?: Integer; } export interface LockRequest { // UUID that identifies the frontend performing the lock @@ -2713,6 +2735,11 @@ export interface MinimalInventoryProduct { // How many units of the product are requested. quantity: Integer; + + // Money pot to use for this product, overrides value from + // the inventory if given. + // Since **v25**. + product_money_pot?: Integer; } export interface PostOrderResponse { @@ -2862,6 +2889,7 @@ export interface CheckPaymentClaimedResponse { // Status URL, can be used as a redirect target for the browser // to show the order QR code / trigger the wallet. + // Since protocol **v19**. order_status_url: string; } @@ -2877,7 +2905,7 @@ export interface CheckPaymentUnpaidResponse { // Deadline when the offer expires; the customer must pay before. // @since protocol **v21**. - pay_deadline: Timestamp | undefined; + pay_deadline?: Timestamp; // Order summary text. summary: string; @@ -2994,8 +3022,8 @@ export interface TransferList { transfers: TransferDetails[]; } -export interface IncomingTransferList { - incoming: IncomingTransferDetails[]; +export interface ExpectedTransferList { + incoming: ExpectedTransferDetails[]; } export interface TransferDetails { @@ -3015,22 +3043,17 @@ export interface TransferDetails { // Used for filtering via offset. 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. + // Time of the execution of the wire transfer. + // Missing if unknown in protocol **v25**. execution_time?: Timestamp; - // True if we checked the exchange's answer and are happy with it. - // False if we have an answer and are unhappy, missing if we - // do not have an answer from the exchange. - verified?: 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). - confirmed?: boolean; + // True if this wire transfer was expected. + // (a matching "/private/incoming" record exists). + // Since protocol **v20**. + expected?: boolean; } -export interface IncomingTransferDetails { +export interface ExpectedTransferDetails { // How much was wired to the merchant (minus fees). expected_credit_amount?: AmountString; @@ -3713,7 +3736,7 @@ export interface OrderCommon { fulfillment_message_i18n?: { [lang_tag: string]: string }; // List of products that are part of the purchase (see Product). - products?: Product[]; + products?: ProductSold[]; // Time when this contract was generated. timestamp?: Timestamp; @@ -3771,6 +3794,13 @@ export interface OrderCommon { // Minimum age buyer must have (in years). Default is 0. minimum_age?: Integer; + + // Money pot to increment for whatever order payment amount + // is not yet assigned to a pot via the ProductSold. + // Not useful to wallets, only for + // merchant-internal accounting. + // Since protocol **v25**. + order_default_money_pot?: Integer; } export enum OrderVersion { @@ -3802,7 +3832,7 @@ export interface OrderV1 extends OrderCommon { choices?: OrderChoice[]; } -export interface Product { +export interface ProductSold { // Merchant-internal identifier for the product. product_id?: string; @@ -3835,6 +3865,12 @@ export interface Product { // Time indicating when this product should be delivered. delivery_date?: Timestamp; + + // Money pot to use for this product, overrides value from + // the inventory if given. Not useful to wallets, only for + // merchant-internal accounting. + // Since **v25**. + product_money_pot?: Integer; } export interface Tax { @@ -4016,6 +4052,7 @@ export const codecForTalerMerchantConfigResponse = ) .property("version", codecForString()) .property("currencies", codecForMap(codecForCurrencySpecificiation())) + .property("report_generators", codecForMap(codecForString())) .property("exchanges", codecForList(codecForExchangeConfigInfo())) .property("implementation", codecOptional(codecForString())) .property( @@ -4351,12 +4388,10 @@ export const codecForCategoryProductList = (): Codec<CategoryProductList> => .property("products", codecForList(codecForProductSummary())) .build("TalerMerchantApi.CategoryProductList"); -export const codecForProductSummary = (): Codec<ProductSummary> => - buildCodecForObject<ProductSummary>() +export const codecForProductSummary = (): Codec<CategoryProductSummary> => + buildCodecForObject<CategoryProductSummary>() .property("product_id", codecForString()) - // .property("description", codecForString()) - // .property("description_i18n", codecForInternationalizedString()) - .build("TalerMerchantApi.ProductSummary"); + .build("TalerMerchantApi.CategoryProductSummary"); export const codecForInventorySummaryResponse = (): Codec<InventorySummaryResponse> => @@ -4401,8 +4436,8 @@ export const codecForFullInventoryDetailsResponse = .property("products", codecForList(codecForMerchantPosProductDetail())) .build("TalerMerchantApi.FullInventoryDetailsResponse"); -export const codecForProductDetail = (): Codec<ProductDetail> => - buildCodecForObject<ProductDetail>() +export const codecForProductDetailResponse = (): Codec<ProductDetailResponse> => + buildCodecForObject<ProductDetailResponse>() .property("description", codecForString()) .property("description_i18n", codecForInternationalizedString()) .property("unit", codecForString()) @@ -4417,7 +4452,9 @@ export const codecForProductDetail = (): Codec<ProductDetail> => .property("total_sold", codecForNumber()) .property("total_lost", codecForNumber()) .property("minimum_age", codecOptional(codecForNumber())) - .build("TalerMerchantApi.ProductDetail"); + .property("money_pot_id", codecOptional(codecForNumber())) + .property("product_group_id", codecOptional(codecForNumber())) + .build("TalerMerchantApi.ProductDetailResponse"); export const codecForTax = (): Codec<Tax> => buildCodecForObject<Tax>() @@ -4496,8 +4533,9 @@ const codecForOrderCommon = (): ObjectCodec<OrderCommon> => .property("timestamp", codecOptional(codecForTimestamp)) .property("delivery_location", codecOptional(codecForLocation())) .property("delivery_date", codecOptional(codecForTimestamp)) - .property("products", codecOptional(codecForList(codecForProduct()))) + .property("products", codecOptional(codecForList(codecForProductSold()))) .property("extra", codecOptional(codecForAny())) + .property("order_default_money_pot", codecOptional(codecForNumber())) .build("TalerMerchantApi.Order"); export const codecForOrderV0 = (): Codec<OrderV0> => @@ -4571,8 +4609,8 @@ export const codecForOrderOutputTaxReceipt = (): Codec<OrderOutputTaxReceipt> => .property("donau_urls", codecForList(codecForStringURL())) .build("TalerMerchantApi.OrderOutputTaxReceipt"); -export const codecForProduct = (): Codec<Product> => - buildCodecForObject<Product>() +export const codecForProductSold = (): Codec<ProductSold> => + buildCodecForObject<ProductSold>() .property("product_id", codecOptional(codecForString())) .property("product_name", codecOptional(codecForString())) .property("description", codecForString()) @@ -4586,6 +4624,7 @@ export const codecForProduct = (): Codec<Product> => .property("image", codecOptional(codecForString())) .property("taxes", codecOptional(codecForList(codecForTax()))) .property("delivery_date", codecOptional(codecForTimestamp)) + .property("product_money_pot", codecOptional(codecForNumber())) .build("TalerMerchantApi.Product"); export const codecForCheckPaymentPaidResponse = @@ -4678,10 +4717,10 @@ 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 codecForExpectedTansferList = (): Codec<ExpectedTransferList> => + buildCodecForObject<ExpectedTransferList>() + .property("incoming", codecForList(codecForExpectedTransferDetails())) + .build("TalerMerchantApi.ExpectedTransferList"); export const codecForTransferDetails = (): Codec<TransferDetails> => buildCodecForObject<TransferDetails>() @@ -4691,25 +4730,24 @@ export const codecForTransferDetails = (): Codec<TransferDetails> => .property("exchange_url", codecForURLString()) .property("transfer_serial_id", codecForNumber()) .property("execution_time", codecOptional(codecForTimestamp)) - .property("verified", codecOptional(codecForBoolean())) - .property("confirmed", codecOptional(codecForBoolean())) + .property("expected", codecOptional(codecForBoolean())) .build("TalerMerchantApi.TransferDetails"); -export const codecForIncomingTransferDetails = - (): Codec<IncomingTransferDetails> => - buildCodecForObject<IncomingTransferDetails>() +export const codecForExpectedTransferDetails = + (): Codec<ExpectedTransferDetails> => + buildCodecForObject<ExpectedTransferDetails>() .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("expected_transfer_serial_id", codecOptional(codecForNumber())) + .property("execution_time", codecOptional(codecForTimestamp)) .property("validated", codecForBoolean()) .property("confirmed", codecForBoolean()) .property("last_http_status", codecForNumber()) .property("last_ec", codecForNumber()) .property("last_error_detail", codecOptional(codecForAny())) - .build("TalerMerchantApi.IncomingTransferDetails"); + .build("TalerMerchantApi.ExpectedTransferDetails"); export const codecForOtpDeviceSummaryResponse = (): Codec<OtpDeviceSummaryResponse> => @@ -5011,3 +5049,197 @@ export interface MerchantPostDonauBody { donau_url: string; charity_id: number; } + +export interface MerchantStatisticsReportResponse { + // Name of the business for which the report is generated. + business_name: string; + + // Starting date for the report. + start_date: Timestamp; + + // End date for the report. + end_date: Timestamp; + + // Period of time covered by each bucket (aka granularity). + bucket_period: RelativeTime; + + // Charts to include in the report. + charts: MerchantReportChart[]; +} + +export const codecForMerchantStatisticsReportResponse = + (): Codec<MerchantStatisticsReportResponse> => + buildCodecForObject<MerchantStatisticsReportResponse>() + .property("business_name", codecForString()) + .property("start_date", codecForTimestamp) + .property("end_date", codecForTimestamp) + .property("bucket_period", codecForDuration) + .property("charts", codecForList(codecForMerchantReportChart())) + .build("TalerMerchantApi.MerchantStatisticsReportResponse"); + +export interface MerchantReportChart { + // Name of the chart. + chart_name: string; + + // Label to use for the y-axis of the chart. + // (x-axis is always time). + y_label: string; + + // Statistical values for the respective time windows, + // one entry per bucket_period in between start_date + // and end_date. + data_groups: BucketDataGroup[]; + + // Human-readable labels for the values in each of the + // data_groups. Length of the array must match the + // length of the values arrays. + labels: string[]; + + // Should the values in each of the data_groups + // be rendered cumulatively or using a grouped representation? + cumulative: boolean; +} + +export const codecForMerchantReportChart = (): Codec<MerchantReportChart> => + buildCodecForObject<MerchantReportChart>() + .property("chart_name", codecForString()) + .property("y_label", codecForString()) + .property("data_groups", codecForList(codecForBucketDataGroup())) + .property("labels", codecForList(codecForString())) + .property("cumulative", codecForBoolean()) + .build("TalerMerchantApi.MerchantReportChart"); + +export interface BucketDataGroup { + // Starting data for this group + start_date: Timestamp; + + // Values in the data group. + values: number[]; +} +export const codecForBucketDataGroup = (): Codec<BucketDataGroup> => + buildCodecForObject<BucketDataGroup>() + .property("start_date", codecForTimestamp) + .property("values", codecForList(codecForNumber())) + .build("TalerMerchantApi.BucketDataGroup"); + +export interface ReportGenerationRequest { + // Report token authorizing the report generation. + report_token: string; +} + +export interface ReportAddRequest { + // Description of the report. Possibly included + // in the report message. + description: string; + + // Merchant backend configuration section specifying + // the program to use to transmit the report + program_section: string; + + // Mime-type to request from the data source. + mime_type: string; + + // Base URL to request the data from. + data_source: string; + + // Address where the report program should send + // the report. + target_address: string; + + // Report frequency + report_frequency: RelativeTime; + + // Report frequency shift. Defaults to zero if missing. + report_frequency_shift?: RelativeTime; +} + +export interface ReportAddedResponse { + // Unique ID for the report. + report_serial_id: Integer; +} + +export const codecForReportAddedResponse = (): Codec<ReportAddedResponse> => + buildCodecForObject<ReportAddedResponse>() + .property("report_serial_id", codecForNumber()) + .build("TalerMerchantApi.ReportAddedResponse"); + +export interface ReportDetailResponse { + // Report identifier + report_serial: Integer; + + // Description of the report. Possibly included + // in the report message. + description: string; + + // Merchant backend configuration section specifying + // the program to use to transmit the report + program_section: string; + + // Mime-type to request from the data source. + mime_type: string; + + // Base URL to request the data from. + data_source: string; + + // Address where the report program should send + // the report. + target_address: string; + + // Report frequency + report_frequency: RelativeTime; + + // Report frequency shift + report_frequency_shift: RelativeTime; + + // Numeric error code unique to the + // error encountered in generating the latest report. + // Absent if there was no error. + last_error_code?: Integer; + + // Details about any error encountered + // in generating the latest report. + last_error_detail?: string; +} + +export const codecForReportDetailResponse = (): Codec<ReportDetailResponse> => + buildCodecForObject<ReportDetailResponse>() + .property("report_serial", codecForNumber()) + .property("description", codecForString()) + .property("program_section", codecForString()) + .property("mime_type", codecForString()) + .property("data_source", codecForString()) + .property("target_address", codecForString()) + .property("report_frequency", codecForDuration) + .property("report_frequency_shift", codecForDuration) + .property("last_error_code", codecOptional(codecForNumber())) + .property("last_error_detail", codecOptional(codecForString())) + .build("TalerMerchantApi.ReportDetailResponse"); + +export interface ReportsSummaryResponse { + // Return reports that are present in our backend. + reports: ReportEntry[]; +} + +export const codecForReportsSummaryResponse = + (): Codec<ReportsSummaryResponse> => + buildCodecForObject<ReportsSummaryResponse>() + .property("reports", codecForList(codecForReportEntry())) + .build("TalerMerchantApi.ReportsSummaryResponse"); + +export interface ReportEntry { + // Report identifier + report_serial: Integer; + + // Description for the report. + description: string; + + // Frequency for the report. + report_frequency: RelativeTime; +} + +export const codecForReportEntry = (): Codec<ReportEntry> => + buildCodecForObject<ReportEntry>() + .property("report_serial", codecForNumber()) + .property("description", codecForString()) + .property("report_frequency", codecForDuration) + .build("TalerMerchantApi.ReportEntry"); diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -4454,7 +4454,6 @@ async function processPurchaseAbortingRefund( checkDbInvariant(!!coin, `coin not found for ${coinPub}`); abortingCoins.push({ coin_pub: coinPub, - contribution: Amounts.stringify(payCoinSelection.coinContributions[i]), exchange_url: coin.exchangeBaseUrl, }); } diff --git a/packages/taler-wallet-webextension/src/components/ProductList.tsx b/packages/taler-wallet-webextension/src/components/ProductList.tsx @@ -14,12 +14,12 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, Product } from "@gnu-taler/taler-util"; +import { Amounts, ProductSold } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; import { SmallLightText } from "./styled/index.js"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -export function ProductList({ products }: { products: Product[] }): VNode { +export function ProductList({ products }: { products: ProductSold[] }): VNode { const { i18n } = useTranslationContext(); return ( <Fragment> diff --git a/packages/taler-wallet-webextension/src/cta/Refund/index.ts b/packages/taler-wallet-webextension/src/cta/Refund/index.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { AmountJson, Product } from "@gnu-taler/taler-util"; +import { AmountJson } from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; import { ErrorAlert } from "../../context/alert.js"; @@ -33,7 +33,6 @@ export type State = | State.Loading | State.LoadingUriError | State.Ready - // | State.InProgress | State.Ignored; export namespace State {