gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] 02/05: from star to alert, using amounts api


From: gnunet
Subject: [taler-merchant-backoffice] 02/05: from star to alert, using amounts api
Date: Thu, 24 Jun 2021 14:30:11 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

commit a75d12bb47ceaf17648a8fcc7c9d0e515ae2fcae
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Jun 22 11:20:49 2021 -0300

    from star to alert, using amounts api
---
 .../frontend/src/components/form/FormProvider.tsx  |   2 +-
 packages/frontend/src/components/form/Input.tsx    |   4 +-
 .../frontend/src/components/form/InputDate.tsx     |   7 +-
 .../frontend/src/components/form/InputGroup.tsx    |   5 +-
 .../src/components/form/InputWithAddon.tsx         |   5 +-
 packages/frontend/src/components/form/useField.tsx |  16 ++-
 .../frontend/src/components/form/useGroupField.tsx |   7 +-
 .../src/components/product/ProductForm.tsx         |   1 +
 packages/frontend/src/declaration.d.ts             |   3 -
 .../frontend/src/paths/admin/create/CreatePage.tsx |   2 +-
 .../instance/orders/create/Create.stories.tsx      |   2 +-
 .../paths/instance/orders/create/CreatePage.tsx    | 130 ++++++++++++++-------
 .../src/paths/instance/orders/list/Table.tsx       |  21 ++--
 .../paths/instance/products/create/CreatePage.tsx  |   2 +-
 .../paths/instance/products/update/UpdatePage.tsx  |   2 +-
 .../paths/instance/reserves/create/CreatePage.tsx  |   4 +-
 .../paths/instance/transfers/create/CreatePage.tsx |   2 +-
 .../src/paths/instance/update/UpdatePage.tsx       |   9 +-
 packages/frontend/src/utils/amount.ts              |  15 +--
 packages/frontend/src/utils/constants.ts           |   2 +-
 20 files changed, 137 insertions(+), 104 deletions(-)

diff --git a/packages/frontend/src/components/form/FormProvider.tsx 
b/packages/frontend/src/components/form/FormProvider.tsx
index d1fb77b..4855553 100644
--- a/packages/frontend/src/components/form/FormProvider.tsx
+++ b/packages/frontend/src/components/form/FormProvider.tsx
@@ -65,7 +65,7 @@ export function useFormContext<T>() {
 }
 
 export type FormErrors<T> = {
-  [P in keyof T]?: string
+  [P in keyof T]?: string | FormErrors<T[P]>
 }
 
 export type FormtoStr<T> = {
diff --git a/packages/frontend/src/components/form/Input.tsx 
b/packages/frontend/src/components/form/Input.tsx
index 4147f2c..f7e0b5c 100644
--- a/packages/frontend/src/components/form/Input.tsx
+++ b/packages/frontend/src/components/form/Input.tsx
@@ -59,8 +59,8 @@ export function Input<T>({ name, readonly, placeholder, 
tooltip, label, expand,
             onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => 
onChange(fromStr(e.currentTarget.value))} />
           {help}
           {children}
-          { required && <span class="icon is-danger is-right">
-            <i class="mdi mdi-star" />
+          { required && <span class="icon has-text-danger is-right">
+            <i class="mdi mdi-alert" />
           </span> }
         </p>
         {error && <p class="help is-danger">{error}</p>}
diff --git a/packages/frontend/src/components/form/InputDate.tsx 
b/packages/frontend/src/components/form/InputDate.tsx
index 654d608..614e44a 100644
--- a/packages/frontend/src/components/form/InputDate.tsx
+++ b/packages/frontend/src/components/form/InputDate.tsx
@@ -36,7 +36,7 @@ export function InputDate<T>({ name, readonly, label, 
placeholder, help, tooltip
   const [opened, setOpened] = useState(false)
   const i18n = useTranslator()
 
-  const { error, value, onChange } = useField<T>(name);
+  const { error, required, value, onChange } = useField<T>(name);
 
   let strValue = ''
   if (!value) {
@@ -61,12 +61,15 @@ export function InputDate<T>({ name, readonly, label, 
placeholder, help, tooltip
     <div class="field-body is-flex-grow-3">
       <div class="field">
         <div class="field has-addons">
-          <p class={expand ? "control is-expanded" : "control"}>
+          <p class={expand ? "control is-expanded has-icons-right" : "control 
has-icons-right"}>
             <input class="input" type="text"
               readonly value={strValue}
               placeholder={placeholder}
               onClick={() => { if (!readonly) setOpened(true) }}
             />
+          { required && <span class="icon has-text-danger is-right">
+              <i class="mdi mdi-alert" />
+            </span> }
             {help}
           </p>
           <div class="control" onClick={() => { if (!readonly) setOpened(true) 
}}>
diff --git a/packages/frontend/src/components/form/InputGroup.tsx 
b/packages/frontend/src/components/form/InputGroup.tsx
index 0720cfb..8d1f512 100644
--- a/packages/frontend/src/components/form/InputGroup.tsx
+++ b/packages/frontend/src/components/form/InputGroup.tsx
@@ -36,11 +36,14 @@ export function InputGroup<T>({ name, label, children, 
tooltip, alternative }: P
 
   return <div class="card">
     <header class="card-header">
-      <p class={!group?.hasError ? "card-header-title" : "card-header-title 
has-text-danger"}>
+      <p class="card-header-title">
         {label}
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
+        {group?.hasError && <span class="icon has-text-danger" 
data-tooltip={tooltip}>
+          <i class="mdi mdi-alert" />
+        </span>}
       </p>
       <button class="card-header-icon" aria-label="more options" onClick={(): 
void => setActive(!active)}>
         <span class="icon">
diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx 
b/packages/frontend/src/components/form/InputWithAddon.tsx
index cb84c86..d2905df 100644
--- a/packages/frontend/src/components/form/InputWithAddon.tsx
+++ b/packages/frontend/src/components/form/InputWithAddon.tsx
@@ -19,7 +19,6 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 import { ComponentChildren, h, VNode } from "preact";
-import { useTranslator } from "../../i18n";
 import { InputProps, useField } from "./useField";
 
 export interface Props<T> extends InputProps<T> {
@@ -60,8 +59,8 @@ export function InputWithAddon<T>({ name, readonly, 
addonBefore, children, expan
               placeholder={placeholder} readonly={readonly}
               name={String(name)} value={toStr(value)}
               onChange={(e): void => onChange(fromStr(e.currentTarget.value))} 
/>
-            {required && <span class="icon is-danger is-right">
-              <i class="mdi mdi-star" />
+            {required && <span class="icon has-text-danger is-right">
+              <i class="mdi mdi-alert" />
             </span>}
             {help}
             {children}
diff --git a/packages/frontend/src/components/form/useField.tsx 
b/packages/frontend/src/components/form/useField.tsx
index c552711..a04be70 100644
--- a/packages/frontend/src/components/form/useField.tsx
+++ b/packages/frontend/src/components/form/useField.tsx
@@ -29,7 +29,7 @@ interface Use<V> {
   initial: any;
   onChange: (v: V) => void;
   toStr: (f: V | undefined) => string;
-  fromStr: (v: string) => V 
+  fromStr: (v: string) => V
 }
 
 export function useField<T>(name: keyof T): Use<T[typeof name]> {
@@ -42,16 +42,20 @@ export function useField<T>(name: keyof T): Use<T[typeof 
name]> {
       return setValueDeeper(prev, String(field).split('.'), value)
     })
   }
-  
-  const defaultToString = ((f?: V):string => String(!f ? '': f))
-  const defaultFromString = ((v: string):V => v as any)
+
+  const defaultToString = ((f?: V): string => String(!f ? '' : f))
+  const defaultFromString = ((v: string): V => v as any)
   const value = readField(object, String(name))
   const initial = readField(initialObject, String(name))
   const isDirty = value !== initial
   const hasError = readField(errors, String(name))
+  if (name == 'pricing.order_price') {
+
+    console.log(value, initial, value === initial)
+  }
   return {
     error: isDirty ? hasError : undefined,
-    required: !isDirty && hasError, 
+    required: !isDirty && hasError,
     value,
     initial,
     onChange: updateField(name) as any,
@@ -73,7 +77,7 @@ const readField = (object: any, name: string) => {
 const setValueDeeper = (object: any, names: string[], value: any): any => {
   if (names.length === 0) return value
   const [head, ...rest] = names
-  return {...object, [head]: setValueDeeper(object[head] || {}, rest, value) }
+  return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) }
 }
 
 export interface InputProps<T> {
diff --git a/packages/frontend/src/components/form/useGroupField.tsx 
b/packages/frontend/src/components/form/useGroupField.tsx
index 0a809e4..a73f464 100644
--- a/packages/frontend/src/components/form/useGroupField.tsx
+++ b/packages/frontend/src/components/form/useGroupField.tsx
@@ -30,8 +30,11 @@ export function useGroupField<T>(name: keyof T): Use {
   if (!f)
     return {};
 
-  const RE = new RegExp(`^${name}`);
   return {
-    hasError: Object.keys(f.errors).some(e => RE.test(e))
+    hasError: readField(f.errors, String(name))
   };
 }
+
+const readField = (object: any, name: string) => {
+  return name.split('.').reduce((prev, current) => prev && prev[current], 
object)
+}
diff --git a/packages/frontend/src/components/product/ProductForm.tsx 
b/packages/frontend/src/components/product/ProductForm.tsx
index 09b5744..9e8ac97 100644
--- a/packages/frontend/src/components/product/ProductForm.tsx
+++ b/packages/frontend/src/components/product/ProductForm.tsx
@@ -51,6 +51,7 @@ export function ProductForm({ onSubscribe, initial, 
alreadyExist, }: Props) {
     taxes: [],
     next_restock: { t_ms: 'never' },
     ...initial,
+    price: ':0',
     stock: !initial || initial.total_stock === -1 ? undefined : {
       current: initial.total_stock || 0,
       lost: initial.total_lost || 0,
diff --git a/packages/frontend/src/declaration.d.ts 
b/packages/frontend/src/declaration.d.ts
index 82bc694..1722a3d 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -844,9 +844,6 @@ export namespace MerchantBackend {
             fulfillment_url?: string;
         }
 
-        // FIXME: Where is this being used?
-        // type ProductSpecification = (MinimalInventoryProduct | Product);
-
         interface MinimalInventoryProduct {
             // Which product is requested (here mandatory!)
             product_id: string;
diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx 
b/packages/frontend/src/paths/admin/create/CreatePage.tsx
index 196284e..c9276d7 100644
--- a/packages/frontend/src/paths/admin/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/admin/create/CreatePage.tsx
@@ -130,7 +130,7 @@ export function CreatePage({ onCreate, onBack, forceId }: 
Props): VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" 
onClick={onBack}><Translate>Cancel</Translate></button>}
             <AsyncButton onClick={submit} disabled={!isTokenSet || hasErrors} 
data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star and 
choose authorization method` : 'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields and choose 
authorization method` : 'confirm operation'
             }><Translate>Confirm</Translate></AsyncButton>
           </div>
 
diff --git 
a/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx 
b/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx
index daf48c0..a1d37e2 100644
--- a/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/Create.stories.tsx
@@ -43,7 +43,7 @@ export const Example = createExample(TestedComponent, {
     default_max_deposit_fee: '',
     default_max_wire_fee: '',
     default_pay_delay: {
-      d_ms: 'forever'
+      d_ms: new Date().getTime()
     },
     default_wire_fee_amortization: 1
   },
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index ecc2f0a..e4d19a8 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -19,10 +19,10 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 
-import { add } from "date-fns";
+import { add, isBefore, isFuture } from "date-fns";
+import { AmountJson, Amounts } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
-import * as yup from 'yup';
 import { FormProvider, FormErrors } from 
"../../../../components/form/FormProvider";
 import { Input } from "../../../../components/form/Input";
 import { InputCurrency } from "../../../../components/form/InputCurrency";
@@ -34,7 +34,7 @@ import { useConfigContext } from "../../../../context/config";
 import { Duration, MerchantBackend, WithId } from "../../../../declaration";
 import { Translate, useTranslator } from "../../../../i18n";
 import { OrderCreateSchema as schema } from '../../../../schemas/index';
-import { multiplyPrice, rate, sumPrices } from "../../../../utils/amount";
+import { rate } from "../../../../utils/amount";
 import { InventoryProductForm } from "./InventoryProductForm";
 import { NonInventoryProductFrom } from "./NonInventoryProductForm";
 
@@ -59,14 +59,15 @@ function with_defaults(config: InstanceConfig): 
Partial<Entity> {
   return {
     inventoryProducts: {},
     products: [],
-    pricing: {} as any,
+    pricing: {
+    },
     payments: {
       max_wire_fee: config.default_max_wire_fee,
       max_fee: config.default_max_deposit_fee,
       wire_fee_amortization: config.default_wire_fee_amortization,
       pay_deadline: defaultPayDeadline,
       refund_deadline: defaultPayDeadline,
-    } as Partial<Payments>,
+    },
     extra: ''
   };
 }
@@ -98,24 +99,60 @@ interface Payments {
 interface Entity {
   inventoryProducts: ProductMap,
   products: MerchantBackend.Product[],
-  pricing: Pricing;
-  payments: Payments;
+  pricing: Partial<Pricing>;
+  payments: Partial<Payments>;
   extra: string;
 }
 
+const stringIsValidJSON = (value: string) => {
+  try {
+    JSON.parse(value.trim())
+    return true
+  } catch {
+    return false
+  }
+}
+
+
 export function CreatePage({ onCreate, onBack, instanceConfig, 
instanceInventory }: Props): VNode {
+  const config = useConfigContext()
+  const zero = Amounts.getZero(config.currency)
   const [value, valueHandler] = useState(with_defaults(instanceConfig))
-  // const [errors, setErrors] = useState<FormErrors<Entity>>({})
 
   const inventoryList = Object.values(value.inventoryProducts || {})
   const productList = Object.values(value.products || {})
 
-  let errors: FormErrors<Entity> = {}
-  try {
-    schema.validateSync(value, { abortEarly: false })
-  } catch (err) {
-    const yupErrors = err.inner as yup.ValidationError[]
-    errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, 
[cur.path]: cur.message }), {})
+  const i18n = useTranslator()
+
+  function check<T>(obj: T): T | undefined {
+    return Object.keys(obj).some(k => (obj as any)[k] !== undefined) ? obj : 
undefined
+  }
+
+  const errors: FormErrors<Entity> = {
+    pricing: {
+      summary: !value.pricing?.summary ? i18n`required`:undefined,
+      order_price: !value.pricing?.order_price ? i18n`required`: (
+        (Amounts.parse(value.pricing.order_price)?.value || 0) <= 0 ? 
i18n`must be greater than 0` : undefined
+      )
+    },
+    extra: value.extra && !stringIsValidJSON(value.extra) ? i18n`not a valid 
json` : undefined,
+    payments: check({
+      refund_deadline: !value.payments?.refund_deadline ? i18n`required` : (
+        !isFuture(value.payments.refund_deadline) ? i18n`should be in the 
future` : (
+          value.payments.pay_deadline && value.payments.refund_deadline && 
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) ?
+            i18n`pay deadline cannot be before refund deadline` : undefined
+        )
+      ),
+      pay_deadline: !value.payments?.pay_deadline ? i18n`required` : (
+        !isFuture(value.payments.pay_deadline) ? i18n`should be in the future` 
: undefined
+      ),
+      auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined 
: (
+        !isFuture(value.payments.auto_refund_deadline) ? i18n`should be in the 
future` : undefined
+      ),
+      delivery_date: !value.payments?.delivery_date ? undefined : (
+        !isFuture(value.payments.delivery_date) ? i18n`should be in the 
future` : undefined
+      ),
+    }),
   }
   const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 
undefined)
 
@@ -147,8 +184,6 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
     onCreate(request);
   }
 
-  const config = useConfigContext()
-
   const addProductToTheInventoryList = (product: 
MerchantBackend.Products.ProductDetail & WithId, quantity: number) => {
     valueHandler(v => {
       const inventoryProducts = { ...v.inventoryProducts }
@@ -182,38 +217,44 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
 
   const [editingProduct, setEditingProduct] = useState<MerchantBackend.Product 
| undefined>(undefined)
 
-  const totalPriceInventory = inventoryList.reduce((prev, cur) => 
sumPrices(prev, multiplyPrice(cur.product.price, cur.quantity)), 
`${config.currency}:0`)
-  const totalPriceProducts = productList.reduce((prev, cur) => sumPrices(prev, 
multiplyPrice(cur.price, cur.quantity)), `${config.currency}:0`)
+  const totalPriceInventory = inventoryList.reduce((prev, cur) => {
+    const p = Amounts.parseOrThrow(cur.product.price)
+    return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount
+  }, zero)
+
+  const totalPriceProducts = productList.reduce((prev, cur) => {
+    const p = Amounts.parseOrThrow(cur.price)
+    return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount
+  }, zero)
 
   const hasProducts = inventoryList.length > 0 || productList.length > 0
-  const totalPrice = sumPrices(totalPriceInventory, totalPriceProducts)
+  const totalPrice = Amounts.add(totalPriceInventory, totalPriceProducts)
 
   useEffect(() => {
     valueHandler(v => {
       return ({
         ...v, pricing: {
           ...v.pricing!,
-          products_price: totalPrice,
-          order_price: totalPrice,
+          // products_price: (Amounts.isZero(totalPrice.amount) ? undefined : 
Amounts.stringify(totalPrice.amount))!,
+          // order_price: (Amounts.isZero(totalPrice.amount) ? undefined : 
Amounts.stringify(totalPrice.amount))!,
+          // products_price: Amounts.stringify(totalPrice.amount),
+          // order_price: Amounts.stringify(totalPrice.amount),
         }
       })
     })
   }, [hasProducts, totalPrice])
 
+  const discountOrRise = rate(value.pricing?.order_price || 
`${config.currency}:0`, Amounts.stringify(totalPrice.amount))
 
-  const discountOrRise = rate(value.pricing?.order_price || 
`${config.currency}:0`, totalPrice)
-
-  useEffect(() => {
-    valueHandler(v => {
-      return ({
-        ...v, pricing: {
-          ...v.pricing!
-        }
-      })
-    })
-  }, [value.pricing?.order_price])
-
-  const i18n = useTranslator()
+  // useEffect(() => {
+  //   valueHandler(v => {
+  //     return ({
+  //       ...v, pricing: {
+  //         ...v.pricing!
+  //       }
+  //     })
+  //   })
+  // }, [value.pricing?.order_price])
 
   return <div>
 
@@ -223,11 +264,10 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
         <div class="column is-four-fifths">
 
           <InputGroup name="inventory_products" label={i18n`Manage products 
from inventory in order`} alternative={
-            inventoryList.length > 0 &&
-            // FIXME: translating plural singular
-            <p>
+            inventoryList.length > 0 && <p>
+              {/* // FIXME: translating plural singular */}
               {inventoryList.length} products
-              with a total price of {totalPriceInventory}.
+              with a total price of {Amounts.stringify(totalPriceInventory)}.
             </p>
           } tooltip={i18n`Manage list of products from managed inventory 
included in the order.`}>
             <InventoryProductForm
@@ -249,9 +289,9 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
 
           <InputGroup name="products" label={i18n`Manage products outside of 
inventory in order`} alternative={
             productList.length > 0 && <p>
-            // FIXME: translating plural singular
+            {/* // FIXME: translating plural singular */}
               {productList.length} products
-              with a total price of {totalPriceProducts}.
+              with a total price of {Amounts.stringify(totalPriceProducts)}.
             </p>
           } tooltip={i18n`Manage list of products without inventory management 
included in the order.`}>
             <NonInventoryProductFrom productToEdit={editingProduct} 
onAddProduct={(p) => {
@@ -280,7 +320,7 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
                 <InputCurrency name="pricing.products_price" label={i18n`Total 
price`} readonly tooltip={i18n`total product price added up`} />
                 <InputCurrency name="pricing.order_price"
                   label={i18n`Total price`}
-                  addonAfter={value.pricing?.order_price !== totalPrice && 
(discountOrRise < 1 ?
+                  addonAfter={discountOrRise > 0 && (discountOrRise < 1 ?
                     `discount of %${Math.round((1 - discountOrRise) * 100)}` :
                     `rise of %${Math.round((discountOrRise - 1) * 100)}`)
                   }
@@ -302,14 +342,14 @@ export function CreatePage({ onCreate, onBack, 
instanceConfig, instanceInventory
                 <InputLocation name="payments.delivery_location" />
               </InputGroup>}
 
-              <InputCurrency name="payments.max_fee" label={i18n`Maximum 
deposit fee`}  tooltip={i18n`Maximum deposit fees the merchant is willing to 
cover for this order. Higher deposit fees must be covered in full by the 
consumer.`} />
-              <InputCurrency name="payments.max_wire_fee" label={i18n`Maximum 
wire fee`} tooltip={i18n`Maximum aggregate wire fees the merchant is willing to 
cover for this order. Wire fees exceeding this amount are to be covered by the 
customers.`}/>
-              <Input name="payments.wire_fee_amortization" label={i18n`Wire 
fee amortization`} tooltip={i18n`Factor by which wire fees exceeding the above 
threshold are divided to determine the share of excess wire fees to be paid 
explicitly by the consumer.`}/>
+              <InputCurrency name="payments.max_fee" label={i18n`Maximum 
deposit fee`} tooltip={i18n`Maximum deposit fees the merchant is willing to 
cover for this order. Higher deposit fees must be covered in full by the 
consumer.`} />
+              <InputCurrency name="payments.max_wire_fee" label={i18n`Maximum 
wire fee`} tooltip={i18n`Maximum aggregate wire fees the merchant is willing to 
cover for this order. Wire fees exceeding this amount are to be covered by the 
customers.`} />
+              <Input name="payments.wire_fee_amortization" label={i18n`Wire 
fee amortization`} tooltip={i18n`Factor by which wire fees exceeding the above 
threshold are divided to determine the share of excess wire fees to be paid 
explicitly by the consumer.`} />
               <Input name="payments.fullfilment_url" label={i18n`Fulfillment 
URL`} tooltip={i18n`URL to which the user will be redirected after successful 
payment.`} />
             </InputGroup>
 
             <InputGroup name="extra" label={i18n`Additional information`} 
tooltip={i18n`Custom information to be included in the contract for this 
order.`}>
-              <Input name="extra" inputType="multiline" label={`Value`}  
tooltip={i18n`You must enter a value in JavaScript Object Notation (JSON).`} />
+              <Input name="extra" inputType="multiline" label={`Value`} 
tooltip={i18n`You must enter a value in JavaScript Object Notation (JSON).`} />
             </InputGroup>
           </FormProvider>
 
diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx 
b/packages/frontend/src/paths/instance/orders/list/Table.tsx
index ed8ee3a..64a5d22 100644
--- a/packages/frontend/src/paths/instance/orders/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -30,9 +30,11 @@ import { InputSelector } from 
"../../../../components/form/InputSelector";
 import { ConfirmModal } from "../../../../components/modal";
 import { MerchantBackend, WithId } from "../../../../declaration";
 import { Translate, useTranslator } from "../../../../i18n";
-import { RefundSchema as RefundSchema } from "../../../../schemas";
-import { mergeRefunds, subtractPrices, sumPrices } from 
"../../../../utils/amount";
+import { RefundSchema } from "../../../../schemas";
+import { mergeRefunds } from "../../../../utils/amount";
 import { AMOUNT_ZERO_REGEX } from "../../../../utils/constants";
+import { Amounts } from "@gnu-taler/taler-util";
+import { useConfigContext } from "../../../../context/config";
 
 type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId
 interface Props {
@@ -174,13 +176,14 @@ export function RefundModal({ order, onCancel, onConfirm 
}: RefundModalProps): V
     }
   }
 
-  const refunds = (order.order_status === 'paid' ? order.refund_details : [])
-    .reduce(mergeRefunds, [])
-  const totalRefunded = refunds.map(r => r.amount).reduce((p, c) => 
sumPrices(c, p), ':0')
-  const orderPrice = (order.order_status === 'paid' ? 
order.contract_terms.amount : undefined)
-  const totalRefundable = !orderPrice ? undefined : (refunds.length ? 
subtractPrices(orderPrice, totalRefunded) : orderPrice)
+  const refunds = (order.order_status === 'paid' ? order.refund_details : 
[]).reduce(mergeRefunds, [])
 
-  const isRefundable = totalRefundable && 
!AMOUNT_ZERO_REGEX.test(totalRefundable)
+  const config = useConfigContext()
+  const totalRefunded = refunds.map(r => r.amount).reduce((p, c) => 
Amounts.add(p, Amounts.parseOrThrow(c)).amount, 
Amounts.getZero(config.currency) )
+  const orderPrice = (order.order_status === 'paid' ? 
Amounts.parseOrThrow(order.contract_terms.amount) : undefined)
+  const totalRefundable = !orderPrice ? 
Amounts.getZero(totalRefunded.currency) : (refunds.length ? 
Amounts.sub(orderPrice, totalRefunded).amount : orderPrice)
+
+  const isRefundable = Amounts.isNonZero(totalRefundable)
   //FIXME: parameters in the translation
   return <ConfirmModal description="refund" danger active onCancel={onCancel} 
onConfirm={validateAndConfirm}>
     {refunds.length > 0 && <div class="columns">
@@ -212,7 +215,7 @@ export function RefundModal({ order, onCancel, onConfirm }: 
RefundModalProps): V
 
     {isRefundable && <FormProvider<State> errors={errors} object={form} 
valueHandler={(d) => setValue(d as any)}>
       <InputCurrency<State> name="refund" label={i18n`Refund`} 
tooltip={i18n`amount to be refunded`}>
-        <Translate>Max refundable:</Translate> {totalRefundable}
+        <Translate>Max refundable:</Translate> 
{Amounts.stringify(totalRefundable)}
       </InputCurrency>
       <InputSelector name="mainReason" label={i18n`Reason`} 
values={[i18n`duplicated`, i18n`requested by the customer`, i18n`other`]} 
tooltip={i18n`why this order is being refunded`} />
       {form.mainReason && <Input<State> label={i18n`Description`} 
name="description" tooltip={i18n`more information to give context`} />}
diff --git 
a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
index 0f2411b..ed669f6 100644
--- a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
@@ -53,7 +53,7 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} 
><Translate>Cancel</Translate></button>}
             <AsyncButton onClick={submitForm} data-tooltip={
-              !submitForm ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              !submitForm ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } 
disabled={!submitForm}><Translate>Confirm</Translate></AsyncButton>
           </div>
 
diff --git 
a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx 
b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
index 32d67c0..d7eb3d1 100644
--- a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
@@ -66,7 +66,7 @@ export function UpdatePage({ product, onUpdate, onBack }: 
Props): VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} 
><Translate>Cancel</Translate></button>}
             <AsyncButton onClick={submitForm} data-tooltip={
-              !submitForm ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              !submitForm ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } 
disabled={!submitForm}><Translate>Confirm</Translate></AsyncButton>
           </div>
         </div>
diff --git 
a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
index 05f5e06..2e85cf9 100644
--- a/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/reserves/create/CreatePage.tsx
@@ -92,7 +92,7 @@ function ViewStep({ step, setCurrentStep, reserve, onBack, 
submitForm, setReserv
               setExchangeQueryError(r.message)
             })
           }} data-tooltip={
-            hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+            hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
           } disabled={hasErrors} ><Translate>Next</Translate></AsyncButton>
         </div>
       </Fragment>
@@ -113,7 +113,7 @@ function ViewStep({ step, setCurrentStep, reserve, onBack, 
submitForm, setReserv
         <div class="buttons is-right mt-5">
           {onBack && <button class="button" onClick={() => 
setCurrentStep(Steps.EXCHANGE)} ><Translate>Back</Translate></button>}
           <AsyncButton onClick={submitForm} data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } disabled={hasErrors} 
><Translate>Confirm</Translate></AsyncButton>
         </div>
       </Fragment>
diff --git 
a/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx
index 5c74326..566483e 100644
--- a/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/transfers/create/CreatePage.tsx
@@ -94,7 +94,7 @@ export function CreatePage({ accounts, onCreate, onBack }: 
Props): VNode {
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} 
><Translate>Cancel</Translate></button>}
             <AsyncButton disabled={hasErrors} data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } onClick={submitForm} 
><Translate>Confirm</Translate></AsyncButton>
           </div>
 
diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx 
b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
index 7612d6f..c900192 100644
--- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
@@ -24,13 +24,6 @@ import { useState } from "preact/hooks";
 import * as yup from 'yup';
 import { AsyncButton } from "../../../components/exception/AsyncButton";
 import { FormProvider, FormErrors } from 
"../../../components/form/FormProvider";
-import { Input } from "../../../components/form/Input";
-import { InputCurrency } from "../../../components/form/InputCurrency";
-import { InputDuration } from "../../../components/form/InputDuration";
-import { InputGroup } from "../../../components/form/InputGroup";
-import { InputLocation } from "../../../components/form/InputLocation";
-import { InputPayto } from "../../../components/form/InputPayto";
-import { InputSecured } from "../../../components/form/InputSecured";
 import { UpdateTokenModal } from "../../../components/modal";
 import { useInstanceContext } from "../../../context/instance";
 import { MerchantBackend } from "../../../declaration";
@@ -157,7 +150,7 @@ export function UpdatePage({ onUpdate, onChangeAuth, 
selected, onBack }: Props):
             <button class="button" onClick={onBack} data-tooltip="cancel 
operation"><Translate>Cancel</Translate></button>
 
             <AsyncButton onClick={submit} data-tooltip={
-              hasErrors ? i18n`Need to complete fields marked with a star` : 
'confirm operation'
+              hasErrors ? i18n`Need to complete marked fields` : 'confirm 
operation'
             } disabled={hasErrors} 
><Translate>Confirm</Translate></AsyncButton>
           </div>
         </div>
diff --git a/packages/frontend/src/utils/amount.ts 
b/packages/frontend/src/utils/amount.ts
index 823ad9d..062ddaf 100644
--- a/packages/frontend/src/utils/amount.ts
+++ b/packages/frontend/src/utils/amount.ts
@@ -22,7 +22,7 @@ import { MerchantBackend } from "../declaration";
  * @param two 
  * @returns 
  */
-export const sumPrices = (one: string, two: string) => {
+const sumPrices = (one: string, two: string) => {
   const [currency, valueOne] = one.split(':')
   const [, valueTwo] = two.split(':')
   return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
@@ -55,19 +55,6 @@ export function mergeRefunds(prev: 
MerchantBackend.Orders.RefundDetails[], cur:
   return prev
 }
 
-export const multiplyPrice = (price: string, q: number) => {
-  const a = Amounts.parseOrThrow(price)
-  const r = Amounts.mult(a, q)
-  return Amounts.stringify(r.amount)
-}
-
-export const subtractPrices = (one: string, two: string) => {
-  const a = Amounts.parseOrThrow(one)
-  const b = Amounts.parseOrThrow(two)
-  const r = Amounts.sub(a, b)
-  return Amounts.stringify(r.amount)
-}
-
 export const rate = (one: string, two: string) => {
   const a = Amounts.parseOrThrow(one)
   const b = Amounts.parseOrThrow(two)
diff --git a/packages/frontend/src/utils/constants.ts 
b/packages/frontend/src/utils/constants.ts
index 403adb9..cbf4342 100644
--- a/packages/frontend/src/utils/constants.ts
+++ b/packages/frontend/src/utils/constants.ts
@@ -23,7 +23,7 @@
 export const PAYTO_REGEX = 
/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
 export const PAYTO_WIRE_METHOD_LOOKUP = 
/payto:\/\/([a-zA-Z][a-zA-Z0-9-.]+)\/.*/
 
-export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
+export const AMOUNT_REGEX = /^[a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
 
 export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/
 

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]