aboutsummaryrefslogtreecommitdiff
path: root/packages/auditor-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
diff options
context:
space:
mode:
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.tsx705
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>
-}