diff options
Diffstat (limited to 'packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx')
-rw-r--r-- | packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 705 |
1 files changed, 0 insertions, 705 deletions
diff --git a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx deleted file mode 100644 index 92271f52f..000000000 --- a/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ /dev/null @@ -1,705 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format, isFuture } from "date-fns"; -import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDate } from "../../../../components/form/InputDate.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputGroup } from "../../../../components/form/InputGroup.js"; -import { InputLocation } from "../../../../components/form/InputLocation.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputToggle } from "../../../../components/form/InputToggle.js"; -import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js"; -import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js"; -import { ProductList } from "../../../../components/product/ProductList.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { useSettings } from "../../../../hooks/useSettings.js"; -import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; -import { rate } from "../../../../utils/amount.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; - -interface Props { - onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; - onBack?: () => void; - instanceConfig: InstanceConfig; - instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[]; -} -interface InstanceConfig { - use_stefan: boolean; - default_pay_delay: TalerProtocolDuration; - default_wire_transfer_delay: TalerProtocolDuration; -} - -function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> { - const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay); - const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay); - - return { - inventoryProducts: {}, - products: [], - pricing: {}, - payments: { - max_fee: undefined, - createToken: true, - pay_deadline: (defaultPayDeadline), - refund_deadline: (defaultPayDeadline), - wire_transfer_deadline: (defaultWireDeadline), - }, - shipping: {}, - extra: {}, - }; -} - -interface ProductAndQuantity { - product: MerchantBackend.Products.ProductDetail & WithId; - quantity: number; -} -export interface ProductMap { - [id: string]: ProductAndQuantity; -} - -interface Pricing { - products_price: string; - order_price: string; - summary: string; -} -interface Shipping { - delivery_date?: Date; - delivery_location?: MerchantBackend.Location; - fullfilment_url?: string; -} -interface Payments { - refund_deadline: Duration; - pay_deadline: Duration; - wire_transfer_deadline: Duration; - auto_refund_deadline: Duration; - max_fee?: string; - createToken: boolean; - minimum_age?: number; -} -interface Entity { - inventoryProducts: ProductMap; - products: MerchantBackend.Product[]; - pricing: Partial<Pricing>; - payments: Partial<Payments>; - shipping: Partial<Shipping>; - extra: Record<string, 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 instance_default = with_defaults(instanceConfig, config.currency) - const [value, valueHandler] = useState(instance_default); - const zero = Amounts.zeroOfCurrency(config.currency); - const [settings, updateSettings] = useSettings() - const inventoryList = Object.values(value.inventoryProducts || {}); - const productList = Object.values(value.products || {}); - - const { i18n } = useTranslationContext(); - - const parsedPrice = !value.pricing?.order_price - ? undefined - : Amounts.parse(value.pricing.order_price); - - const errors: FormErrors<Entity> = { - pricing: undefinedIfEmpty({ - summary: !value.pricing?.summary ? i18n.str`required` : undefined, - order_price: !value.pricing?.order_price - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - }), - payments: undefinedIfEmpty({ - refund_deadline: !value.payments?.refund_deadline - ? undefined - : value.payments.pay_deadline && - Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1 - ? i18n.str`refund deadline cannot be before pay deadline` - : value.payments.wire_transfer_deadline && - Duration.cmp( - value.payments.wire_transfer_deadline, - value.payments.refund_deadline, - ) === -1 - ? i18n.str`wire transfer deadline cannot be before refund deadline` - : undefined, - pay_deadline: !value.payments?.pay_deadline - ? i18n.str`required` - : value.payments.wire_transfer_deadline && - Duration.cmp( - value.payments.wire_transfer_deadline, - value.payments.pay_deadline, - ) === -1 - ? i18n.str`wire transfer deadline cannot be before pay deadline` - : undefined, - wire_transfer_deadline: !value.payments?.wire_transfer_deadline - ? i18n.str`required` - : undefined, - auto_refund_deadline: !value.payments?.auto_refund_deadline - ? undefined - : !value.payments?.refund_deadline - ? i18n.str`should have a refund deadline` - : Duration.cmp( - value.payments.refund_deadline, - value.payments.auto_refund_deadline, - ) == -1 - ? i18n.str`auto refund cannot be after refund deadline` - : undefined, - - }), - shipping: undefinedIfEmpty({ - delivery_date: !value.shipping?.delivery_date - ? undefined - : !isFuture(value.shipping.delivery_date) - ? i18n.str`should be in the future` - : undefined, - }), - }; - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submit = (): void => { - const order = value as any; //schema.cast(value); - if (!value.payments) return; - if (!value.shipping) return; - - const request: MerchantBackend.Orders.PostOrderRequest = { - order: { - amount: order.pricing.order_price, - summary: order.pricing.summary, - products: productList, - extra: undefinedIfEmpty(value.extra), - pay_deadline: !value.payments.pay_deadline ? - i18n.str`required` : - AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline)) - ,// : undefined, - wire_transfer_deadline: value.payments.wire_transfer_deadline - ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline)) - : undefined, - refund_deadline: value.payments.refund_deadline - ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline)) - : undefined, - auto_refund: value.payments.auto_refund_deadline - ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline) - : undefined, - max_fee: value.payments.max_fee as string, - - delivery_date: value.shipping.delivery_date - ? { t_s: value.shipping.delivery_date.getTime() / 1000 } - : undefined, - delivery_location: value.shipping.delivery_location, - fulfillment_url: value.shipping.fullfilment_url, - minimum_age: value.payments.minimum_age, - }, - inventory_products: inventoryList.map((p) => ({ - product_id: p.product.id, - quantity: p.quantity, - })), - create_token: value.payments.createToken, - }; - - onCreate(request); - }; - - const addProductToTheInventoryList = ( - product: MerchantBackend.Products.ProductDetail & WithId, - quantity: number, - ) => { - valueHandler((v) => { - const inventoryProducts = { ...v.inventoryProducts }; - inventoryProducts[product.id] = { product, quantity }; - return { ...v, inventoryProducts }; - }); - }; - - const removeProductFromTheInventoryList = (id: string) => { - valueHandler((v) => { - const inventoryProducts = { ...v.inventoryProducts }; - delete inventoryProducts[id]; - return { ...v, inventoryProducts }; - }); - }; - - const addNewProduct = async (product: MerchantBackend.Product) => { - return valueHandler((v) => { - const products = v.products ? [...v.products, product] : []; - return { ...v, products }; - }); - }; - - const removeFromNewProduct = (index: number) => { - valueHandler((v) => { - const products = v.products ? [...v.products] : []; - products.splice(index, 1); - return { ...v, products }; - }); - }; - - const [editingProduct, setEditingProduct] = useState< - MerchantBackend.Product | undefined - >(undefined); - - 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) => { - if (!cur.price) return zero; - 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 = Amounts.add(totalPriceInventory, totalPriceProducts); - - const totalAsString = Amounts.stringify(totalPrice.amount); - const allProducts = productList.concat(inventoryList.map(asProduct)); - - const [newField, setNewField] = useState("") - - useEffect(() => { - valueHandler((v) => { - return { - ...v, - pricing: { - ...v.pricing, - products_price: hasProducts ? totalAsString : undefined, - order_price: hasProducts ? totalAsString : undefined, - }, - }; - }); - }, [hasProducts, totalAsString]); - - const discountOrRise = rate( - parsedPrice ?? Amounts.zeroOfCurrency(config.currency), - totalPrice.amount, - ); - - const minAgeByProducts = allProducts.reduce( - (cur, prev) => - !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age, - 0, - ); - - // if there is no default pay deadline - const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline - // and there is no default wire deadline - const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline - // user required to set the taler options - const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline - - - return ( - <div> - - <section class="section is-main-section"> - <div class="tabs is-toggle is-fullwidth is-small"> - <ul> - <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: false - }) - }}> - <a > - <span><i18n.Translate>Simple</i18n.Translate></span> - </a> - </li> - <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: true - }) - }}> - <a > - <span><i18n.Translate>Advanced</i18n.Translate></span> - </a> - </li> - </ul> - </div> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - {/* // FIXME: translating plural singular */} - <InputGroup - name="inventory_products" - label={i18n.str`Manage products in order`} - alternative={ - allProducts.length > 0 && ( - <p> - {allProducts.length} products with a total price of{" "} - {totalAsString}. - </p> - ) - } - tooltip={i18n.str`Manage list of products in the order.`} - > - <InventoryProductForm - currentProducts={value.inventoryProducts || {}} - onAddProduct={addProductToTheInventoryList} - inventory={instanceInventory} - /> - - {settings.advanceOrderMode && - <NonInventoryProductFrom - productToEdit={editingProduct} - onAddProduct={(p) => { - setEditingProduct(undefined); - return addNewProduct(p); - }} - /> - } - - {allProducts.length > 0 && ( - <ProductList - list={allProducts} - actions={[ - { - name: i18n.str`Remove`, - tooltip: i18n.str`Remove this product from the order.`, - handler: (e, index) => { - if (e.product_id) { - removeProductFromTheInventoryList(e.product_id); - } else { - removeFromNewProduct(index); - setEditingProduct(e); - } - }, - }, - ]} - /> - )} - </InputGroup> - - <FormProvider<Entity> - errors={errors} - object={value} - valueHandler={valueHandler as any} - > - {hasProducts ? ( - <Fragment> - <InputCurrency - name="pricing.products_price" - label={i18n.str`Total price`} - readonly - tooltip={i18n.str`total product price added up`} - /> - <InputCurrency - name="pricing.order_price" - label={i18n.str`Total price`} - addonAfter={ - discountOrRise > 0 && - (discountOrRise < 1 - ? `discount of %${Math.round( - (1 - discountOrRise) * 100, - )}` - : `rise of %${Math.round((discountOrRise - 1) * 100)}`) - } - tooltip={i18n.str`Amount to be paid by the customer`} - /> - </Fragment> - ) : ( - <InputCurrency - name="pricing.order_price" - label={i18n.str`Order price`} - tooltip={i18n.str`final order price`} - /> - )} - - <Input - name="pricing.summary" - inputType="multiline" - label={i18n.str`Summary`} - tooltip={i18n.str`Title of the order to be shown to the customer`} - /> - - {settings.advanceOrderMode && - <InputGroup - name="shipping" - label={i18n.str`Shipping and Fulfillment`} - initialActive - > - <InputDate - name="shipping.delivery_date" - label={i18n.str`Delivery date`} - tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} - /> - {value.shipping?.delivery_date && ( - <InputGroup - name="shipping.delivery_location" - label={i18n.str`Location`} - tooltip={i18n.str`address where the products will be delivered`} - > - <InputLocation name="shipping.delivery_location" /> - </InputGroup> - )} - <Input - name="shipping.fullfilment_url" - label={i18n.str`Fulfillment URL`} - tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} - /> - </InputGroup> - } - - {(settings.advanceOrderMode || requiresSomeTalerOptions) && - <InputGroup - name="payments" - label={i18n.str`Taler payment options`} - tooltip={i18n.str`Override default Taler payment settings for this order`} - > - {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDuration - name="payments.pay_deadline" - label={i18n.str`Payment time`} - help={<DeadlineHelp duration={value.payments?.pay_deadline} />} - withForever - withoutClear - tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`} - side={ - <span> - <button class="button" onClick={() => { - const c = { - ...value, - payments: { - ...(value.payments ?? {}), - pay_deadline: instance_default.payments?.pay_deadline - } - } - valueHandler(c) - }}> - <i18n.Translate>default</i18n.Translate> - </button> - </span> - } - />} - {settings.advanceOrderMode && <InputDuration - name="payments.refund_deadline" - label={i18n.str`Refund time`} - help={<DeadlineHelp duration={value.payments?.refund_deadline} />} - withForever - withoutClear - tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} - side={ - <span> - <button class="button" onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - refund_deadline: instance_default.payments?.refund_deadline - } - }) - }}> - <i18n.Translate>default</i18n.Translate> - </button> - </span> - } - />} - {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDuration - name="payments.wire_transfer_deadline" - label={i18n.str`Wire transfer time`} - help={<DeadlineHelp duration={value.payments?.wire_transfer_deadline} />} - withoutClear - withForever - tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} - side={ - <span> - <button class="button" onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline - } - }) - }}> - <i18n.Translate>default</i18n.Translate> - </button> - </span> - } - />} - {settings.advanceOrderMode && <InputDuration - name="payments.auto_refund_deadline" - label={i18n.str`Auto-refund time`} - help={<DeadlineHelp duration={value.payments?.auto_refund_deadline} />} - tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} - withForever - />} - - {settings.advanceOrderMode && <InputCurrency - name="payments.max_fee" - label={i18n.str`Maximum fee`} - tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} - />} - {settings.advanceOrderMode && <InputToggle - name="payments.createToken" - label={i18n.str`Create token`} - tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} - />} - {settings.advanceOrderMode && <InputNumber - name="payments.minimum_age" - label={i18n.str`Minimum age required`} - tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} - help={ - minAgeByProducts > 0 - ? i18n.str`Min age defined by the producs is ${minAgeByProducts}` - : i18n.str`No product with age restriction in this order` - } - />} - </InputGroup> - } - - {settings.advanceOrderMode && - <InputGroup - name="extra" - label={i18n.str`Additional information`} - tooltip={i18n.str`Custom information to be included in the contract for this order.`} - > - {Object.keys(value.extra ?? {}).map((key) => { - - return <Input - name={`extra.${key}`} - inputType="multiline" - label={key} - tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} - side={ - <button class="button" onClick={(e) => { - if (value.extra && value.extra[key] !== undefined) { - console.log(value.extra) - delete value.extra[key] - } - valueHandler({ - ...value, - }) - }}>remove</button> - } - /> - })} - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Custom field name</i18n.Translate> - <span class="icon has-tooltip-right" data-tooltip={"new extra field"}> - <i class="mdi mdi-information" /> - </span> - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} /> - </p> - </div> - </div> - <button class="button" onClick={(e) => { - setNewField("") - valueHandler({ - ...value, - extra: { - ...(value.extra ?? {}), - [newField]: "" - } - }) - }}>add</button> - </div> - </InputGroup> - } - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <button - class="button is-success" - onClick={submit} - disabled={hasErrors} - > - <i18n.Translate>Confirm</i18n.Translate> - </button> - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} - -function asProduct(p: ProductAndQuantity): MerchantBackend.Product { - return { - product_id: p.product.id, - image: p.product.image, - price: p.product.price, - unit: p.product.unit, - quantity: p.quantity, - description: p.product.description, - taxes: p.product.taxes, - minimum_age: p.product.minimum_age, - }; -} - - -function DeadlineHelp({ duration }: { duration?: Duration }): VNode { - const { i18n } = useTranslationContext(); - const [now, setNow] = useState(AbsoluteTime.now()) - useEffect(() => { - const iid = setInterval(() => { - setNow(AbsoluteTime.now()) - }, 60 * 1000) - return () => { - clearInterval(iid) - } - }) - if (!duration) return <i18n.Translate>Disabled</i18n.Translate> - const when = AbsoluteTime.addDuration(now, duration) - if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate> - return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate> -} |