[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.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-merchant-backoffice] branch master updated: order creation,
gnunet <=