gnunet-svn
[Top][All Lists]
Advanced

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

[taler-merchant-backoffice] branch master updated: order creation


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: order creation
Date: Tue, 13 Apr 2021 20:29:12 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 3ee6de0  order creation
3ee6de0 is described below

commit 3ee6de02c72e7164fdfe7f67ff0678b58e996f93
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Apr 13 15:28:58 2021 -0300

    order creation
---
 CHANGELOG.md                                       |   5 +-
 packages/frontend/src/components/form/Field.tsx    |  10 +
 .../frontend/src/components/form/InputCurrency.tsx |   8 +-
 .../form/{InputWithAddon.tsx => InputDate.tsx}     |  49 ++---
 .../frontend/src/components/form/InputGroup.tsx    |   5 +-
 .../src/components/form/InputSearchProduct.tsx     |  20 +-
 .../src/components/form/InputWithAddon.tsx         |   4 +-
 packages/frontend/src/declaration.d.ts             |  12 +-
 packages/frontend/src/hooks/instance.ts            |   8 +-
 packages/frontend/src/messages/en.po               |  97 ++++++++++
 .../paths/instance/orders/create/CreatePage.tsx    | 204 ++++++++++++++++-----
 .../orders/create/NonInventoryProductForm.tsx      |  20 +-
 .../paths/instance/orders/details/DetailPage.tsx   |  47 +++--
 .../paths/instance/products/create/ProductForm.tsx |  11 +-
 packages/frontend/src/schemas/index.ts             |  93 +++++++---
 15 files changed, 452 insertions(+), 141 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc7b7b8..29fcb9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,10 +26,7 @@ and this project adheres to [Semantic 
Versioning](https://semver.org/spec/v2.0.0
  
  - product detail: we could have some button that brings us to the detailed 
screen for the product
  - order id field to go
-
-frontend, too many redirects
-BUGS TEST CASES:
-https://git.taler.net/anastasis.git/tree/src/cli/test_anastasis_reducer_enter_secret.sh
+ - input number
 
  - navigation to another instance should not do full refresh
  - cleanup instance and token management, because code is a mess and can be 
refactored 
diff --git a/packages/frontend/src/components/form/Field.tsx 
b/packages/frontend/src/components/form/Field.tsx
index 2bb2712..ed50eb7 100644
--- a/packages/frontend/src/components/form/Field.tsx
+++ b/packages/frontend/src/components/form/Field.tsx
@@ -115,6 +115,16 @@ export function useField<T>(name: keyof T) {
   }
 }
 
+export function useGroupField<T>(name: keyof T) {
+  const f = useContext<FormType<T>>(FormContext)
+  if (!f) return {}
+  
+  const RE = new RegExp(`^${name}`)
+  return {
+    hasError: Object.keys(f.errors).some(e => RE.test(e))
+  }
+}
+
 // export function Field<T>({ name, info, readonly }: Props<T>): VNode {
 //   const {errors, object, valueHandler, updateField} = useForm<T>()
 
diff --git a/packages/frontend/src/components/form/InputCurrency.tsx 
b/packages/frontend/src/components/form/InputCurrency.tsx
index 69fa2a8..fa1eba9 100644
--- a/packages/frontend/src/components/form/InputCurrency.tsx
+++ b/packages/frontend/src/components/form/InputCurrency.tsx
@@ -18,7 +18,7 @@
 *
 * @author Sebastian Javier Marchano (sebasjm)
 */
-import { h } from "preact";
+import { ComponentChildren, h } from "preact";
 import { Amount } from "../../declaration";
 import { InputWithAddon } from "./InputWithAddon";
 
@@ -27,14 +27,16 @@ export interface Props<T> {
   readonly?: boolean;
   expand?: boolean;
   currency: string;
+  addonAfter?: ComponentChildren;
 }
 
-export function InputCurrency<T>({ name, readonly, expand, currency }: 
Props<T>) {
+export function InputCurrency<T>({ name, readonly, expand, currency, 
addonAfter }: Props<T>) {
   return <InputWithAddon<T> name={name} readonly={readonly} 
addonBefore={currency}
+    addonAfter={addonAfter}
     inputType='number' expand={expand}
     toStr={(v?: Amount) => v?.split(':')[1] || ''}
     fromStr={(v: string) => !v ? '' : `${currency}:${v}`}
-    inputExtra={{min:0}}
+    inputExtra={{ min: 0 }}
   />
 }
 
diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx 
b/packages/frontend/src/components/form/InputDate.tsx
similarity index 60%
copy from packages/frontend/src/components/form/InputWithAddon.tsx
copy to packages/frontend/src/components/form/InputDate.tsx
index 7124778..ce077e7 100644
--- a/packages/frontend/src/components/form/InputWithAddon.tsx
+++ b/packages/frontend/src/components/form/InputDate.tsx
@@ -18,27 +18,23 @@
 *
 * @author Sebastian Javier Marchano (sebasjm)
 */
-import { ComponentChildren, h, VNode } from "preact";
+import { format } from "date-fns";
+import { ComponentChildren, Fragment, h } from "preact";
 import { Message, useMessage } from "preact-messages";
+import { useState } from "preact/hooks";
+import { Amount } from "../../declaration";
+import { DatePicker } from "./DatePicker";
 import { useField } from "./Field";
+import { InputWithAddon } from "./InputWithAddon";
 
 export interface Props<T> {
   name: keyof T;
   readonly?: boolean;
   expand?: boolean;
-  inputType?: 'text' | 'number';
-  addonBefore?: string | VNode;
-  addonAfter?: string | VNode;
-  toStr?: (v?: any) => string;
-  fromStr?: (s: string) => any;
-  inputExtra?: any,
-  children?: ComponentChildren,
 }
 
-const defaultToString = (f?: any):string => f || ''
-const defaultFromString = (v: string):any => v as any
-
-export function InputWithAddon<T>({ name, readonly, addonBefore, children, 
expand, inputType, inputExtra, addonAfter, toStr = defaultToString, fromStr = 
defaultFromString }: Props<T>): VNode {
+export function InputDate<T>({ name, readonly, expand }: Props<T>) {
+  const [opened, setOpened] = useState(false)
   const { error, value, onChange } = useField<T>(name);
 
   const placeholder = useMessage(`fields.instance.${name}.placeholder`);
@@ -56,23 +52,28 @@ export function InputWithAddon<T>({ name, readonly, 
addonBefore, children, expan
     <div class="field-body is-flex-grow-3">
       <div class="field">
         <div class="field has-addons">
-          {addonBefore && <div class="control">
-            <a class="button is-static">{addonBefore}</a>
-          </div>}
-          <p class={ expand ? "control is-expanded" : "control" }>
-            <input {...(inputExtra||{})} class={error ? "input is-danger" : 
"input"} type={inputType}
-              placeholder={placeholder} readonly={readonly} 
-              name={String(name)} value={toStr(value)}
-              onChange={(e): void => onChange(fromStr(e.currentTarget.value))} 
/>
+          <p class={expand ? "control is-expanded" : "control"}>
+            <input class="input" type="text" 
+              readonly value={!value ? '' : format(value, 'yyyy/MM/dd 
HH:mm:ss')} 
+              placeholder="pick a date" 
+              onClick={() => setOpened(true)}
+              />
             <Message id={`fields.instance.${name}.help`}> </Message>
-            {children}
           </p>
-          {addonAfter && <div class="control">
-            <a class="button is-static">{addonAfter}</a>
-          </div>}
+          <div class="control" onClick={() => setOpened(true)}>
+            <a class="button is-static" >
+              <span class="icon"><i class="mdi mdi-calendar" /></span>
+            </a>
+          </div>
         </div>
         {error ? <p class="help is-danger"><Message 
id={`validation.${error.type}`} 
fields={error.params}>{error.message}</Message></p> : null}
       </div>
     </div>
+    <DatePicker
+      opened={opened}
+      closeFunction={() => setOpened(false)}
+      dateReceiver={(d) => onChange(d as any)}
+    />
   </div>;
 }
+
diff --git a/packages/frontend/src/components/form/InputGroup.tsx 
b/packages/frontend/src/components/form/InputGroup.tsx
index 2e5217b..5d9e551 100644
--- a/packages/frontend/src/components/form/InputGroup.tsx
+++ b/packages/frontend/src/components/form/InputGroup.tsx
@@ -21,6 +21,7 @@
 import { ComponentChildren, h, VNode } from "preact";
 import { Message } from "preact-messages";
 import { useState } from "preact/hooks";
+import { useField, useGroupField } from "./Field";
 
 export interface Props<T> {
   name: keyof T;
@@ -30,9 +31,11 @@ export interface Props<T> {
 
 export function InputGroup<T>({ name, children, alternative}: Props<T>): VNode 
{
   const [active, setActive] = useState(false);
+  const group = useGroupField<T>(name);
+  
   return <div class="card">
     <header class="card-header">
-      <p class="card-header-title">
+      <p class={ !group?.hasError ? "card-header-title" : "card-header-title 
has-text-danger"}>
         <Message id={`fields.instance.${String(name)}.label`} />
       </p>
       <button class="card-header-icon" aria-label="more options" onClick={(): 
void => setActive(!active)}>
diff --git a/packages/frontend/src/components/form/InputSearchProduct.tsx 
b/packages/frontend/src/components/form/InputSearchProduct.tsx
index 9f16340..1bdc94f 100644
--- a/packages/frontend/src/components/form/InputSearchProduct.tsx
+++ b/packages/frontend/src/components/form/InputSearchProduct.tsx
@@ -95,13 +95,21 @@ function ProductList({ name, onSelect }: ProductListProps) {
       <div class="dropdown-item">loading...</div>
     </div>
   } else if (result.ok && !!name) {
-    products = <div class="dropdown-content">
-      {result.data.filter(p => re.test(p.description)).map(p => (
-        <div class="dropdown-item" onClick={() => onSelect(p)}>
-          {p.description}
+    if (!result.data.length) {
+      products = <div class="dropdown-content">
+        <div class="dropdown-item">
+          no products found
         </div>
-      ))}
-    </div>
+      </div>
+    } else {
+      products = <div class="dropdown-content">
+        {result.data.filter(p => re.test(p.description)).map(p => (
+          <div class="dropdown-item" onClick={() => onSelect(p)}>
+            {p.description}
+          </div>
+        ))}
+      </div>
+    }
   }
   return <div class="dropdown is-active">
     <div class="dropdown-menu" id="dropdown-menu" role="menu">
diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx 
b/packages/frontend/src/components/form/InputWithAddon.tsx
index 7124778..a983143 100644
--- a/packages/frontend/src/components/form/InputWithAddon.tsx
+++ b/packages/frontend/src/components/form/InputWithAddon.tsx
@@ -27,8 +27,8 @@ export interface Props<T> {
   readonly?: boolean;
   expand?: boolean;
   inputType?: 'text' | 'number';
-  addonBefore?: string | VNode;
-  addonAfter?: string | VNode;
+  addonBefore?: ComponentChildren;
+  addonAfter?: ComponentChildren;
   toStr?: (v?: any) => string;
   fromStr?: (s: string) => any;
   inputExtra?: any,
diff --git a/packages/frontend/src/declaration.d.ts 
b/packages/frontend/src/declaration.d.ts
index fdecfe3..34370e0 100644
--- a/packages/frontend/src/declaration.d.ts
+++ b/packages/frontend/src/declaration.d.ts
@@ -130,19 +130,19 @@ export namespace MerchantBackend {
         description_i18n?: { [lang_tag: string]: string };
 
         // The number of units of the product to deliver to the customer.
-        quantity?: Integer;
+        quantity: Integer;
 
         // The unit in which the product is measured (liters, kilograms, 
packages, etc.)
-        unit?: string;
+        unit: string;
 
         // The price of the product; this is the total price for quantity 
times unit of this product.
-        price?: Amount;
+        price: Amount;
 
         // An optional base64-encoded product image
-        image?: ImageDataUrl;
+        image: ImageDataUrl;
 
         // a list of taxes paid by the merchant for this product. Can be empty.
-        taxes?: Tax[];
+        taxes: Tax[];
 
         // time indicating when this product should be delivered
         delivery_date?: Timestamp;
@@ -380,7 +380,7 @@ export namespace MerchantBackend {
 
             // If the frontend does NOT specify a payment deadline, how long 
should
             // offers we make be valid by default?
-            default_pay_deadline: RelativeTime;
+            default_pay_delay: RelativeTime;
 
             // Authentication configuration.
             // Does not contain the token when token auth is configured.
diff --git a/packages/frontend/src/hooks/instance.ts 
b/packages/frontend/src/hooks/instance.ts
index c96c1f6..8eabc6b 100644
--- a/packages/frontend/src/hooks/instance.ts
+++ b/packages/frontend/src/hooks/instance.ts
@@ -78,7 +78,13 @@ export function useInstanceDetails(): 
HttpResponse<MerchantBackend.Instances.Que
     url: `${baseUrl}/instances/${id}`, token: instanceToken
   }
 
-  const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, 
HttpError>([`/private/`, token, url], fetcher)
+  const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, 
HttpError>([`/private/`, token, url], fetcher, {
+    refreshInterval:0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+  })
 
   if (isValidating) return {loading:true, data: data?.data}
   if (data) return data
diff --git a/packages/frontend/src/messages/en.po 
b/packages/frontend/src/messages/en.po
index b59ebc6..73b9408 100644
--- a/packages/frontend/src/messages/en.po
+++ b/packages/frontend/src/messages/en.po
@@ -410,3 +410,100 @@ msgstr "Products Taxes"
 
 msgid "fields.instance.pricing.net.label"
 msgstr "Net"
+
+
+msgid "fields.instance.payments.label"
+msgstr "Payments"
+
+
+
+msgid "fields.instance.payments.auto_refund_deadline.label"
+msgstr "Auto Refund Deadline"
+
+
+
+msgid "fields.instance.payments.refund_deadline.label"
+msgstr "Refund Deadline"
+
+
+
+msgid "fields.instance.payments.pay_deadline.label"
+msgstr "Pay Deadline"
+
+
+
+msgid "fields.instance.payments.delivery_date.label"
+msgstr "Delivery Date"
+
+msgid "fields.instance.payments.delivery_location.label"
+msgstr "Delivery Location"
+
+
+
+msgid "fields.instance.payments.max_fee.label"
+msgstr "Max Fee"
+
+
+
+msgid "fields.instance.payments.max_wire_fee.label"
+msgstr "Max Wire Fee"
+
+
+
+msgid "fields.instance.payments.wire_fee_amortization.label"
+msgstr "Wire Fee Amortization"
+
+
+
+msgid "fields.instance.payments.fullfilment_url.label"
+msgstr "Fillfilment URL"
+
+
+
+msgid "fields.instance.payments.delivery_location.country.label"
+msgstr "Country"
+
+
+
+msgid "fields.instance.payments.delivery_location.address_lines.label"
+msgstr "Adress Lines"
+
+
+
+msgid "fields.instance.payments.delivery_location.building_number.label"
+msgstr "Building Number"
+
+
+
+msgid "fields.instance.payments.delivery_location.building_name.label"
+msgstr "Building Name"
+
+
+
+msgid "fields.instance.payments.delivery_location.street.label"
+msgstr "Stree"
+
+
+
+msgid "fields.instance.payments.delivery_location.post_code.label"
+msgstr "Post Code"
+
+
+
+msgid "fields.instance.payments.delivery_location.town_location.label"
+msgstr "Town Location"
+
+msgid "fields.instance.payments.delivery_location.town.label"
+msgstr "Town"
+
+msgid "fields.instance.payments.delivery_location.district.label"
+msgstr "District"
+
+msgid "fields.instance.payments.delivery_location.country_subdivision.label"
+msgstr "Country Subdivision"
+
+msgid "fields.instance.extra.label"
+msgstr "Extra information"
+
+msgid "fields.instance.extra.tooltip"
+msgstr "Must be a JSON formatted string"
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index f3bdfa9..d5b4527 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -30,6 +30,11 @@ import { InventoryProductForm } from 
"./InventoryProductForm";
 import { NonInventoryProductFrom } from "./NonInventoryProductForm";
 import { InputCurrency } from "../../../../components/form/InputCurrency";
 import { Input } from "../../../../components/form/Input";
+import { OrderCreateSchema as schema } from '../../../../schemas/index';
+import * as yup from 'yup';
+import { InputDate } from "../../../../components/form/InputDate";
+import { useInstanceDetails } from "../../../../hooks/instance";
+import { add } from "date-fns";
 
 interface Props {
   onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@@ -40,7 +45,9 @@ function with_defaults(): Entity {
   return {
     inventoryProducts: {},
     products: [],
-    pricing: {} as any
+    pricing: {} as any,
+    payments: {} as any,
+    extra: ''
   };
 }
 
@@ -58,27 +65,57 @@ interface Pricing {
   order_price: string;
   summary: string;
 }
+interface Payments {
+  refund_deadline?: Date;
+  pay_deadline?: Date;
+  auto_refund_deadline?: Date;
+  delivery_date?: Date;
+  delivery_location?: MerchantBackend.Location;
+  max_fee?: string;
+  max_wire_fee?: string;
+  wire_fee_amortization?: number;
+  fullfilment_url?: string;
+}
 interface Entity {
   inventoryProducts: ProductMap,
-  products: MerchantBackend.Products.ProductAddDetail[],
+  products: MerchantBackend.Product[],
   pricing: Pricing;
+  payments: Payments;
+  extra:string;
 }
 
 export function CreatePage({ onCreate, onBack }: Props): VNode {
   const [value, valueHandler] = useState(with_defaults())
   const [errors, setErrors] = useState<FormErrors<Entity>>({})
 
-  // const submit = (): void => {
-  //   try {
-  //     // schema.validateSync(value, { abortEarly: false })
-  //     // const order = schema.cast(value) as Entity
-  //     // onCreate({ order });
-  //   } catch (err) {
-  //     const errors = err.inner as yup.ValidationError[]
-  //     const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : 
({ ...prev, [cur.path]: { type: cur.type, params: cur.params, message: 
cur.message } }), {})
-  //     setErrors(pathMessages)
-  //   }
-  // }
+  const inventoryList = Object.values(value.inventoryProducts)
+  const productList = Object.values(value.products)
+
+  const submit = (): void => {
+    try {
+      schema.validateSync(value, { abortEarly: false })
+      const order = schema.cast(value)
+
+      const request: MerchantBackend.Orders.PostOrderRequest = {
+        order: {
+          amount: order.pricing.order_price,
+          summary: order.pricing.summary,
+          products: productList,
+          extra: value.extra,
+        },
+        inventory_products: inventoryList.map(p => ({
+          product_id: p.product.id,
+          quantity: p.quantity
+        })),
+      }
+      onCreate(request);
+    } catch (err) {
+      const errors = err.inner as yup.ValidationError[]
+      const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ 
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message 
} }), {})
+      setErrors(pathMessages)
+    }
+  }
+
   const config = useConfigContext()
 
   const addProductToTheInventoryList = (product: 
MerchantBackend.Products.ProductDetail & WithId, quantity: number) => {
@@ -97,7 +134,7 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
     })
   }
 
-  const addNewProduct = (product: MerchantBackend.Products.ProductAddDetail) 
=> {
+  const addNewProduct = (product: MerchantBackend.Product) => {
     valueHandler(v => {
       const products = [...v.products, product]
       return ({ ...v, products })
@@ -112,16 +149,13 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
     })
   }
 
-  const [editingProduct, setEditingProduct] = 
useState<MerchantBackend.Products.ProductAddDetail | undefined>(undefined)
+  const [editingProduct, setEditingProduct] = useState<MerchantBackend.Product 
| undefined>(undefined)
 
-  const inventoryList = Object.values(value.inventoryProducts)
-  const productList = Object.values(value.products)
-
-  const totalPriceInventory = inventoryList.reduce((prev, cur) => 
sumPrices(multiplyPrice(cur.product.price, cur.quantity), prev), ':0')
-  const totalPriceProducts = productList.reduce((prev, cur) => 
sumPrices(multiplyPrice(cur.price, cur.total_stock), prev), ':0')
+  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 totalTaxInventory = inventoryList.reduce((prev, cur) => 
sumPrices(multiplyPrice(cur.product.taxes.reduce((prev, cur) => 
sumPrices(cur.tax, prev), ':0'), cur.quantity), prev), ':0')
-  const totalTaxProducts = productList.reduce((prev, cur) => 
sumPrices(multiplyPrice(cur.taxes.reduce((prev, cur) => sumPrices(cur.tax, 
prev), ':0'), cur.total_stock), prev), ':0')
+  const totalTaxInventory = inventoryList.reduce((prev, cur) => 
sumPrices(prev, multiplyPrice(cur.product.taxes.reduce((prev, cur) => 
sumPrices(prev, cur.tax), `${config.currency}:0`), cur.quantity)), 
`${config.currency}:0`)
+  const totalTaxProducts = productList.reduce((prev, cur) => sumPrices(prev, 
multiplyPrice(cur.taxes.reduce((prev, cur) => sumPrices(prev, cur.tax), 
`${config.currency}:0`), cur.quantity)), `${config.currency}:0`)
 
   const hasProducts = inventoryList.length > 0 || productList.length > 0
   const totalPrice = sumPrices(totalPriceInventory, totalPriceProducts)
@@ -129,15 +163,50 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
 
   useEffect(() => {
     valueHandler(v => {
-      return ({...v, pricing: {
-        ...v.pricing,
-        products_price: totalPrice,
-        products_taxes: totalTax,
-        order_price: totalPrice,
-        net: subtractPrices(totalPrice, totalTax),
-      }})
+      return ({
+        ...v, pricing: {
+          ...v.pricing,
+          products_price: totalPrice,
+          products_taxes: totalTax,
+          order_price: totalPrice,
+          net: subtractPrices(totalPrice, totalTax),
+        }
+      })
     })
-  }, [hasProducts, totalPrice, totalTax, value.pricing])
+  }, [hasProducts, totalPrice, totalTax])
+
+
+  const discountOrRise = rate(value.pricing.order_price, totalPrice)
+  useEffect(() => {
+    valueHandler(v => {
+      return ({
+        ...v, pricing: {
+          ...v.pricing,
+          net: subtractPrices(v.pricing.order_price, totalTax),
+        }
+      })
+    })
+  }, [value.pricing.order_price])
+
+  const details_response = useInstanceDetails()
+
+  useEffect(() => {
+    if (details_response.ok) {
+      valueHandler(v => {
+        const defaultPayDeadline = !details_response.data.default_pay_delay || 
details_response.data.default_pay_delay.d_ms === "forever" ? undefined : 
add(new Date(), {seconds: details_response.data.default_pay_delay.d_ms/1000})
+        return ({
+          ...v, payments: {
+            ...v.payments,
+            max_wire_fee: details_response.data.default_max_wire_fee,
+            max_fee: details_response.data.default_max_deposit_fee,
+            wire_fee_amortization: 
details_response.data.default_wire_fee_amortization,
+            pay_deadline: defaultPayDeadline,
+            refund_deadline: defaultPayDeadline,
+          }
+        })
+      })
+    }
+  }, [details_response.ok])
 
   return <div>
 
@@ -198,7 +267,7 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
           <InputGroup name="products" alternative={
             productList.length > 0 && <p>
               {productList.length} products,
-              in {productList.reduce((prev, cur) => cur.total_stock + prev, 
0)} units,
+              in {productList.reduce((prev, cur) => cur.quantity + prev, 0)} 
units,
               with a total price of {totalPriceProducts}
             </p>
           }>
@@ -224,10 +293,10 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
                       <td>image</td>
                       <td >{entry.description}</td>
                       <td >
-                        {entry.total_stock} {entry.unit}
+                        {entry.quantity} {entry.unit}
                       </td>
                       <td >{entry.price}</td>
-                      <td >{multiplyPrice(entry.price, entry.total_stock)}</td>
+                      <td >{multiplyPrice(entry.price, entry.quantity)}</td>
                       <td class="is-actions-cell right-sticky">
                         <div class="buttons is-right">
                           <button class="button is-small is-success jb-modal" 
type="button" onClick={(): void => {
@@ -249,22 +318,59 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
           </InputGroup>
 
           <FormProvider<Entity> errors={errors} object={value} 
valueHandler={valueHandler as any}>
-            {hasProducts ? <Fragment>
-              <InputCurrency name="pricing.products_price" readonly  
currency={config.currency}/>
-              <InputCurrency name="pricing.products_taxes" readonly 
currency={config.currency}/>
-              <InputCurrency name="pricing.order_price" 
currency={config.currency} />
-              <InputCurrency name="pricing.net" readonly 
currency={config.currency} />
-            </Fragment> : <Fragment>
+            {hasProducts ?
+              <Fragment>
+                <InputCurrency name="pricing.products_price" readonly 
currency={config.currency} />
+                <InputCurrency name="pricing.products_taxes" readonly 
currency={config.currency} />
+                <InputCurrency name="pricing.order_price" 
currency={config.currency}
+                  addonAfter={value.pricing.order_price !== totalPrice && 
(discountOrRise < 1 ?
+                    `discount of %${Math.round((1 - discountOrRise) * 100)}` :
+                    `rise of %${Math.round((discountOrRise - 1) * 100)}`)
+                  }
+                />
+                <InputCurrency name="pricing.net" readonly 
currency={config.currency} />
+              </Fragment> :
               <InputCurrency name="pricing.order_price" 
currency={config.currency} />
-            </Fragment>}
-
-            <Input name="pricing.summary" />
-
+            }
+
+            <Input name="pricing.summary" inputType="multiline" />
+
+
+            <InputGroup name="payments">
+              <InputDate name="payments.auto_refund_deadline" />
+              <InputDate name="payments.refund_deadline" />
+              <InputDate name="payments.pay_deadline" />
+
+              <InputDate name="payments.delivery_date" />
+              { value.payments.delivery_date && <InputGroup 
name="payments.delivery_location" >
+                <Input name="payments.delivery_location.country" />
+                <Input name="payments.delivery_location.address_lines" 
inputType="multiline"
+                  toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')}
+                  fromStr={(v: string) => v.split('\n')}
+                />
+                <Input name="payments.delivery_location.building_number" />
+                <Input name="payments.delivery_location.building_name" />
+                <Input name="payments.delivery_location.street" />
+                <Input name="payments.delivery_location.post_code" />
+                <Input name="payments.delivery_location.town_location" />
+                <Input name="payments.delivery_location.town" />
+                <Input name="payments.delivery_location.district" />
+                <Input name="payments.delivery_location.country_subdivision" />
+              </InputGroup> }
+
+              <InputCurrency name="payments.max_fee" 
currency={config.currency} />
+              <InputCurrency name="payments.max_wire_fee" 
currency={config.currency} />
+              <Input name="payments.wire_fee_amortization" />
+              <Input name="payments.fullfilment_url" />
+            </InputGroup>
+            <InputGroup name="extra">
+              <Input name="extra" inputType="multiline" />
+            </InputGroup>
           </FormProvider>
 
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} ><Message 
id="Cancel" /></button>}
-            {/* <button class="button is-success" onClick={submit} ><Message 
id="Confirm" /></button> */}
+            <button class="button is-success" onClick={submit} ><Message 
id="Confirm" /></button>
           </div>
 
         </div>
@@ -293,3 +399,13 @@ const subtractPrices = (one: string, two: string) => {
   const [, valueTwo] = two.split(':')
   return `${currency}:${parseInt(valueOne, 10) - parseInt(valueTwo, 10)}`
 }
+
+const rate = (one?: string, two?: string) => {
+  const [, valueOne] = (one || '').split(':')
+  const [, valueTwo] = (two || '').split(':')
+  const intOne = parseInt(valueOne, 10)
+  const intTwo = parseInt(valueTwo, 10)
+  if (!intTwo) return intOne
+  return intOne / intTwo
+}
+
diff --git 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
 
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
index 36e8bac..8426264 100644
--- 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
+++ 
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
@@ -5,7 +5,7 @@ import { MerchantBackend } from "../../../../declaration";
 import { useListener } from "../../../../hooks";
 import { ProductForm } from "../../products/create/ProductForm";
 
-type Entity = MerchantBackend.Products.ProductAddDetail
+type Entity = MerchantBackend.Product
 
 interface Props {
   onAddProduct: (p: Entity) => void;
@@ -20,19 +20,31 @@ export function NonInventoryProductFrom({ value, 
onAddProduct }: Props) {
     setShowCreateProduct(editing)
   }, [editing])
 
-  const [ submitForm, addFormSubmitter ] = useListener<Entity | 
undefined>((result) => {
+  const [ submitForm, addFormSubmitter ] = 
useListener<Partial<MerchantBackend.Products.ProductAddDetail> | 
undefined>((result) => {
     if (result) {
       setShowCreateProduct(false)
-      onAddProduct(result)
+      onAddProduct({
+        quantity: result.total_stock || 0,
+        taxes: result.taxes || [],
+        description: result.description || '',
+        image: result.image || '',
+        price: result.price || '',
+        unit: result.unit || ''
+      })
     }
   })
+
+  const initial: Partial<MerchantBackend.Products.ProductAddDetail> = {
+    ...value,
+    total_stock: value?.quantity || 0,
+  } 
   
   return <Fragment>
     <div class="buttons">
       <button class="button is-success" onClick={() => 
setShowCreateProduct(true)} >add new product</button>
     </div>
     {showCreateProduct && <ConfirmModal active onCancel={() => 
setShowCreateProduct(false)} onConfirm={submitForm}>
-      <ProductForm initial={value} onSubscribe={addFormSubmitter} />
+      <ProductForm initial={initial} onSubscribe={addFormSubmitter} />
     </ConfirmModal>}
   </Fragment>
 }
\ No newline at end of file
diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx 
b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
index f317762..f631858 100644
--- a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
+++ b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
@@ -37,7 +37,7 @@ interface Props {
   onBack: () => void;
   selected: Entity;
   id: string;
-  onRefund: (id:string, value: MerchantBackend.Orders.RefundRequest) => void;
+  onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void;
 }
 
 interface KeyValue {
@@ -132,8 +132,9 @@ function ClaimedPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.
                       textOverflow: 'ellipsis',
                       // maxWidth: '100%',
                     }}>
-                      <p>pay at: <a 
href={order.contract_terms.fulfillment_url} rel="nofollow" 
target="new">{order.contract_terms.fulfillment_url}</a></p>
-                      <p>{format(new 
Date(order.contract_terms.timestamp.t_ms), 'yyyy/MM/dd HH:mm:ss')}</p>
+                      {/* <a href={order.order_status_url} rel="nofollow" 
target="new">{order.order_status_url}</a> */}
+                      <p>pay at:  <b>missing value, there is no 
order_status_url</b></p>
+                      <p>created at: {format(new 
Date(order.contract_terms.timestamp.t_ms), 'yyyy-MM-dd HH:mm:ss')}</p>
                     </div>
                   </div>
                 </div>
@@ -179,7 +180,7 @@ function ClaimedPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.
     </section>
   </div>
 }
-function PaidPage({ id, order, onRefund }: { id: string; order: 
MerchantBackend.Orders.CheckPaymentPaidResponse, onRefund: (id:string) => void 
}) {
+function PaidPage({ id, order, onRefund }: { id: string; order: 
MerchantBackend.Orders.CheckPaymentPaidResponse, onRefund: (id: string) => void 
}) {
   const events: Event[] = []
   events.push({
     when: new Date(),
@@ -226,19 +227,19 @@ function PaidPage({ id, order, onRefund }: { id: string; 
order: MerchantBackend.
     })
   })
   if (order.contract_terms.wire_transfer_deadline.t_ms !== 'never' &&
-  order.contract_terms.wire_transfer_deadline.t_ms < new Date().getTime() ) 
events.push({
-    when: new Date(order.contract_terms.wire_transfer_deadline.t_ms - 1000*10),
-    description: `wired (faked)`,
-    type: 'wired',
-  })
+    order.contract_terms.wire_transfer_deadline.t_ms < new Date().getTime()) 
events.push({
+      when: new Date(order.contract_terms.wire_transfer_deadline.t_ms - 1000 * 
10),
+      description: `wired (faked)`,
+      type: 'wired',
+    })
 
   events.sort((a, b) => a.when.getTime() - b.when.getTime())
-  const [value, valueHandler] = useState<Partial<Paid>>({...order, fee: 
'COL:0.1'} as any)
+  const [value, valueHandler] = useState<Partial<Paid>>({ ...order, fee: 
'COL:0.1' } as any)
   const [errors, setErrors] = useState<KeyValue>({})
   const config = useConfigContext()
 
   const refundable = !order.refunded &&
-      new Date().getTime() <order.contract_terms.refund_deadline.t_ms
+    new Date().getTime() < order.contract_terms.refund_deadline.t_ms
 
   return <div>
     <section class="section">
@@ -275,7 +276,7 @@ function PaidPage({ id, order, onRefund }: { id: string; 
order: MerchantBackend.
                   <div class="level-item">
                     <h1 class="title">
                       <div class="buttons">
-                        { refundable && <button class="button is-danger" 
onClick={() => onRefund(id) }>refund</button> }
+                        {refundable && <button class="button is-danger" 
onClick={() => onRefund(id)}>refund</button>}
                         <button class="button is-info" onClick={() => {
                           if (order.contract_terms.fulfillment_url) 
copyToClipboard(order.contract_terms.fulfillment_url)
                         }}>copy url</button>
@@ -315,7 +316,7 @@ function PaidPage({ id, order, onRefund }: { id: string; 
order: MerchantBackend.
                   <Input name="contract_terms.summary" readonly 
inputType="multiline" />
                   <InputCurrency name="contract_terms.amount" readonly 
currency={config.currency} />
                   <InputCurrency name="fee" readonly 
currency={config.currency} />
-                  { order.refunded && <InputCurrency<Paid> 
name="refund_amount" readonly currency={config.currency} /> }
+                  {order.refunded && <InputCurrency<Paid> name="refund_amount" 
readonly currency={config.currency} />}
                   <InputCurrency<Paid> name="deposit_total" readonly 
currency={config.currency} />
                   <Input<Paid> name="order_status" readonly />
                 </FormProvider>
@@ -347,6 +348,22 @@ function UnpaidPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.C
             <div class="tag is-dark">unpaid</div>
           </div>
         </div>
+
+        <div class="level">
+          <div class="level-left" style={{ maxWidth: '100%' }}>
+            <div class="level-item" style={{ maxWidth: '100%' }}>
+              <div class="content" style={{
+                whiteSpace: 'nowrap',
+                overflow: 'hidden',
+                textOverflow: 'ellipsis',
+                // maxWidth: '100%',
+              }}>
+                <p>pay at: <a href={order.order_status_url} rel="nofollow" 
target="new">{order.order_status_url}</a></p>
+                <p>created at: <b>missing value, there is no contract term 
yet</b></p>
+              </div>
+            </div>
+          </div>
+        </div>
       </div>
     </section>
 
@@ -370,7 +387,7 @@ function UnpaidPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.C
 export function DetailPage({ id, selected, onRefund }: Props): VNode {
   const [showRefund, setShowRefund] = useState<string | undefined>(undefined)
 
-  const DetailByStatus = function (){
+  const DetailByStatus = function () {
     switch (selected.order_status) {
       case 'claimed': return <ClaimedPage id={id} order={selected} />
       case 'paid': return <PaidPage id={id} order={selected} onRefund={(order) 
=> setShowRefund(id)} />
@@ -378,7 +395,7 @@ export function DetailPage({ id, selected, onRefund }: 
Props): VNode {
       default: return <div>unknown order status</div>
     }
   }
-  
+
   return <Fragment>
     <NotificationCard notification={{
       message: 'DEMO WARNING',
diff --git 
a/packages/frontend/src/paths/instance/products/create/ProductForm.tsx 
b/packages/frontend/src/paths/instance/products/create/ProductForm.tsx
index 6a1cbff..ac059b7 100644
--- a/packages/frontend/src/paths/instance/products/create/ProductForm.tsx
+++ b/packages/frontend/src/paths/instance/products/create/ProductForm.tsx
@@ -12,20 +12,17 @@ type Entity = MerchantBackend.Products.ProductAddDetail
 
 interface Props {
   onSubscribe: (c:() => Entity|undefined) => void;
-  initial?: Entity;
+  initial?: Partial<Entity>;
 }
 
 export function ProductForm({onSubscribe, initial}:Props) {
-  const [value, valueHandler] = useState<Partial<Entity>>(initial || {
-    taxes:[]
-  })
+  const [value, valueHandler] = useState<Partial<Entity>>(initial||{})
   const [errors, setErrors] = useState<FormErrors<Entity>>({})
 
   const submit = useCallback((): Entity|undefined => {
     try {
       schema.validateSync(value, { abortEarly: false })
-      return schema.cast(value) as any as Entity
-      // onCreate(schema.cast(value) as any as Entity );
+      return value as MerchantBackend.Products.ProductAddDetail
     } catch (err) {
       const errors = err.inner as yup.ValidationError[]
       const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ 
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message 
} }), {})
@@ -44,7 +41,7 @@ export function ProductForm({onSubscribe, initial}:Props) {
 
       <Input<Entity> name="description" />
       <InputCurrency<Entity> name="price" currency={config.currency} />
-      <Input<Entity> name="total_stock" inputType="number" />
+      <Input<Entity> name="total_stock" inputType="number"  fromStr={(v) => 
parseInt(v, 10)} toStr={(v) => ""+v} inputExtra={{min:0}} />
 
     </FormProvider>
   </div>
diff --git a/packages/frontend/src/schemas/index.ts 
b/packages/frontend/src/schemas/index.ts
index 6e6288b..4cf766a 100644
--- a/packages/frontend/src/schemas/index.ts
+++ b/packages/frontend/src/schemas/index.ts
@@ -14,11 +14,12 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
 
+import { isAfter, isFuture } from 'date-fns';
 import * as yup from 'yup';
 import { AMOUNT_REGEX, PAYTO_REGEX } from "../utils/constants";
 
@@ -45,8 +46,21 @@ function listOfPayToUrisAreValid(values?: (string | 
undefined)[]): boolean {
 function currencyWithAmountIsValid(value?: string): boolean {
   return !!value && AMOUNT_REGEX.test(value)
 }
+function currencyGreaterThan0(value?: string) {
+  if (value) {
+    try {
+      const [,amount] = value.split(':')
+      const intAmount = parseInt(amount,10)
+      return intAmount > 0
+    } catch {
+      return false
+    }
+  }
+  return true
+}
+
 export const InstanceSchema = yup.object().shape({
-  id: yup.string().required().meta({type: 'url'}),
+  id: yup.string().required().meta({ type: 'url' }),
   name: yup.string().required(),
   auth: yup.object().shape({
     method: yup.string().matches(/^(external|token)$/),
@@ -54,16 +68,16 @@ export const InstanceSchema = yup.object().shape({
   }),
   payto_uris: yup.array().of(yup.string())
     .min(1)
-    .meta({type: 'array'})
+    .meta({ type: 'array' })
     .test('payto', '{path} is not valid', listOfPayToUrisAreValid),
   default_max_deposit_fee: yup.string()
     .required()
     .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
-    .meta({type: 'amount'}),
+    .meta({ type: 'amount' }),
   default_max_wire_fee: yup.string()
     .required()
     .test('amount', '{path} is not valid', currencyWithAmountIsValid)
-    .meta({type: 'amount'}),
+    .meta({ type: 'amount' }),
   default_wire_fee_amortization: yup.number()
     .required(),
   address: yup.object().shape({
@@ -77,7 +91,7 @@ export const InstanceSchema = yup.object().shape({
     town: yup.string(),
     district: yup.string().optional(),
     country_subdivision: yup.string().optional(),
-  }).meta({type:'group'}),
+  }).meta({ type: 'group' }),
   jurisdiction: yup.object().shape({
     country: yup.string().optional(),
     address_lines: yup.array().of(yup.string()).max(7).optional(),
@@ -89,42 +103,73 @@ export const InstanceSchema = yup.object().shape({
     town: yup.string(),
     district: yup.string().optional(),
     country_subdivision: yup.string().optional(),
-  }).meta({type:'group'}),
+  }).meta({ type: 'group' }),
   default_pay_delay: yup.object()
     .shape({ d_ms: yup.number() })
     .required()
     .meta({ type: 'duration' }),
-    // .transform(numberToDuration),
+  // .transform(numberToDuration),
   default_wire_transfer_delay: yup.object()
     .shape({ d_ms: yup.number() })
     .required()
     .meta({ type: 'duration' }),
-    // .transform(numberToDuration),
+  // .transform(numberToDuration),
 })
 
 export const InstanceUpdateSchema = InstanceSchema.clone().omit(['id']);
 export const InstanceCreateSchema = InstanceSchema.clone();
 
 export const RefoundSchema = yup.object().shape({
-  mainReason: yup.string().required(), 
-  description: yup.string().required(), 
+  mainReason: yup.string().required(),
+  description: yup.string().required(),
   refund: yup.string()
     .required()
-    .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
+    .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
+    .test('amount_positive', 'the amount is not valid', currencyGreaterThan0),
 })
 
+const stringIsValidJSON = (value?: string) => {
+  const p = value?.trim()
+  if (!p) return true;
+  try {
+    JSON.parse(p)
+    return true
+  } catch {
+    return false
+  }
+}
 
 export const OrderCreateSchema = yup.object().shape({
-  summary: yup.string().required(), 
-  amount: yup.string()
-    .required()
-    .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
+  pricing: yup.object().required().shape({
+    summary: yup.string().ensure().required(),
+    order_price: yup.string()
+      .ensure()
+      .required()
+      .test('amount', 'the amount is not valid', currencyWithAmountIsValid)
+      .test('amount_positive', 'the amount should be greater than 0', 
currencyGreaterThan0),
+  }),
+  extra: yup.string().test('extra', 'is not a JSON format', stringIsValidJSON),
+  payments: yup.object().required().shape({
+    refund_deadline: yup.date()
+      .test('future', 'should be in the future', (d) => d ? isFuture(d) : 
true),
+    pay_deadline: yup.date()
+      .test('future', 'should be in the future', (d) => d ? isFuture(d) : 
true),
+    auto_refund_deadline: yup.date()
+      .test('future', 'should be in the future', (d) => d ? isFuture(d) : 
true),
+    delivery_date: yup.date()
+      .test('future', 'should be in the future', (d) => d ? isFuture(d) : 
true),
+  }).test('payment', 'dates', (d) => {
+    if (d.pay_deadline && d.refund_deadline && isAfter(d.refund_deadline, 
d.pay_deadline)) {
+      return new yup.ValidationError('pay deadline should be greater than 
refund','asd','payments.pay_deadline')
+    }
+    return true
+  })
 })
 
 export const ProductCreateSchema = yup.object().shape({
-  description: yup.string().required(), 
-  price:yup.string()
-  .required()
-  .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
-  total_stock: yup.number().required(), 
+  description: yup.string().required(),
+  price: yup.string()
+    .required()
+    .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
+  total_stock: yup.number().required(),
 })

-- 
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]