commit 2444fb8a6d3160fe2ed639207a7856f026913fd4
parent fb5a844be86c40e0fea2985092724e69f7a39374
Author: Sebastian <sebasjm@gmail.com>
Date: Tue, 13 Apr 2021 17:56:43 -0300
refunded table
Diffstat:
8 files changed, 144 insertions(+), 54 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -25,7 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- check if there is a way to remove auto async for /routes /components/{async,routes} so it can be turned on when building non-single-bundle
- product detail: we could have some button that brings us to the detailed screen for the product
- - order id field to go
- input number
- navigation to another instance should not do full refresh
diff --git a/packages/frontend/src/components/form/InputCurrency.tsx b/packages/frontend/src/components/form/InputCurrency.tsx
@@ -28,15 +28,17 @@ export interface Props<T> {
expand?: boolean;
currency: string;
addonAfter?: ComponentChildren;
+ children?: ComponentChildren;
}
-export function InputCurrency<T>({ name, readonly, expand, currency, addonAfter }: Props<T>) {
+export function InputCurrency<T>({ name, readonly, expand, currency, addonAfter, children }: 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 }}
+ children={children}
/>
}
diff --git a/packages/frontend/src/components/form/InputGroup.tsx b/packages/frontend/src/components/form/InputGroup.tsx
@@ -26,17 +26,18 @@ import { useField, useGroupField } from "./Field";
export interface Props<T> {
name: keyof T;
children: ComponentChildren;
+ description?: string;
alternative?: ComponentChildren;
}
-export function InputGroup<T>({ name, children, alternative}: Props<T>): VNode {
+export function InputGroup<T>({ name, description, 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={ !group?.hasError ? "card-header-title" : "card-header-title has-text-danger"}>
- <Message id={`fields.instance.${String(name)}.label`} />
+ { description ? description : <Message id={`fields.instance.${String(name)}.label`} /> }
</p>
<button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}>
<span class="icon">
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -35,6 +35,7 @@ import * as yup from 'yup';
import { InputDate } from "../../../../components/form/InputDate";
import { useInstanceDetails } from "../../../../hooks/instance";
import { add } from "date-fns";
+import { multiplyPrice, rate, subtractPrices, sumPrices } from "../../../../utils/amount";
interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@@ -102,6 +103,13 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
summary: order.pricing.summary,
products: productList,
extra: value.extra,
+ pay_deadline: value.payments.pay_deadline ? { t_ms: value.payments.pay_deadline.getTime()*1000 } : undefined,
+ wire_transfer_deadline: value.payments.pay_deadline ? { t_ms: value.payments.pay_deadline.getTime()*1000 } : undefined,
+ refund_deadline: value.payments.refund_deadline ? { t_ms: value.payments.refund_deadline.getTime()*1000 } : undefined,
+ max_fee: value.payments.max_fee,
+ max_wire_fee: value.payments.max_wire_fee,
+ delivery_date: value.payments.delivery_date ? { t_ms: value.payments.delivery_date.getTime()*1000 } : undefined,
+ delivery_location: value.payments.delivery_location,
},
inventory_products: inventoryList.map(p => ({
product_id: p.product.id,
@@ -382,30 +390,3 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
}
-const multiplyPrice = (price: string, q: number) => {
- const [currency, value] = price.split(':')
- const total = parseInt(value, 10) * q
- return `${currency}:${total}`
-}
-
-const sumPrices = (one: string, two: string) => {
- const [currency, valueOne] = one.split(':')
- const [, valueTwo] = two.split(':')
- return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
-}
-
-const subtractPrices = (one: string, two: string) => {
- const [currency, valueOne] = one.split(':')
- 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/details/DetailPage.tsx b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
@@ -31,6 +31,7 @@ import { copyToClipboard } from "../../../../utils/functions";
import { format } from "date-fns";
import { Event, Timeline } from "./Timeline";
import { RefundModal } from "../list/Table";
+import { mergeRefunds } from "../../../../utils/amount";
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
interface Props {
@@ -212,10 +213,10 @@ function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend.
description: 'delivery',
type: 'delivery'
})
- order.refund_details.forEach(e => {
+ order.refund_details.reduce(mergeRefunds,[]).forEach(e => {
events.push({
when: new Date(e.timestamp.t_ms),
- description: `refund: ${e.amount}`,
+ description: `refund: ${e.amount}: ${e.reason}`,
type: 'refund',
})
})
@@ -238,8 +239,7 @@ function PaidPage({ id, order, onRefund }: { id: string; order: MerchantBackend.
const [errors, setErrors] = useState<KeyValue>({})
const config = useConfigContext()
- const refundable = !order.refunded &&
- new Date().getTime() < order.contract_terms.refund_deadline.t_ms
+ const refundable = new Date().getTime() < order.contract_terms.refund_deadline.t_ms
return <div>
<section class="section">
@@ -408,6 +408,7 @@ export function DetailPage({ id, selected, onRefund }: Props): VNode {
{DetailByStatus()}
{showRefund && <RefundModal
+ id={id}
onCancel={() => setShowRefund(undefined)}
onConfirm={(value) => {
onRefund(showRefund, value)
diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -26,12 +26,15 @@ import { StateUpdater, useCallback, useEffect, useRef, useState } from "preact/h
import { FormErrors, FormProvider } from "../../../../components/form/Field";
import { Input } from "../../../../components/form/Input";
import { InputCurrency } from "../../../../components/form/InputCurrency";
+import { InputGroup } from "../../../../components/form/InputGroup";
import { InputSelector } from "../../../../components/form/InputSelector";
import { ConfirmModal } from "../../../../components/modal";
import { useConfigContext } from "../../../../context/backend";
import { MerchantBackend, WithId } from "../../../../declaration"
+import { useOrderDetails } from "../../../../hooks/order";
import { RefoundSchema } from "../../../../schemas";
-import { AMOUNT_REGEX } from "../../../../utils/constants";
+import { mergeRefunds, subtractPrices, sumPrices } from "../../../../utils/amount";
+import { AMOUNT_ZERO_REGEX } from "../../../../utils/constants";
import { Actions, buildActions } from "../../../../utils/table";
type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId
@@ -86,6 +89,7 @@ export function CardTable({ instances, onCreate, onRefund, onCopyURL, onSelect,
</div>
</div>
{showRefund && <RefundModal
+ id={showRefund}
onCancel={() => setShowRefund(undefined)}
onConfirm={(value) => {
onRefund(showRefund, value)
@@ -120,7 +124,7 @@ function Table({ instances, onSelect, onRefund, onCopyURL, onLoadMoreAfter, onLo
</tr>
</thead>
<tbody>
- {instances.map((i,pos) => {
+ {instances.map((i, pos) => {
return <tr>
<td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{format(new Date(i.timestamp.t_ms), 'yyyy/MM/dd HH:mm:ss')}</td>
<td onClick={(): void => onSelect(i)} style={{ cursor: 'pointer' }} >{i.amount}</td>
@@ -158,11 +162,13 @@ function EmptyTable(): VNode {
interface RefundModalProps {
onCancel: () => void;
+ id: string;
onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void;
}
-export function RefundModal({ onCancel, onConfirm }: RefundModalProps): VNode {
+export function RefundModal({ id, onCancel, onConfirm }: RefundModalProps): VNode {
const config = useConfigContext()
+ const result = useOrderDetails(id)
type State = { mainReason?: string, description?: string, refund?: string }
const [form, setValue] = useState<State>({})
@@ -183,14 +189,50 @@ export function RefundModal({ onCancel, onConfirm }: RefundModalProps): VNode {
}
}
- return <ConfirmModal description="delete_instance" danger active onCancel={onCancel} onConfirm={validateAndConfirm}>
- <div class="block">
- You are going to refund the order
- </div>
- <FormProvider<State> errors={errors} object={form} valueHandler={(d) => setValue(d as any)}>
- <InputCurrency<State> name="refund" currency={config.currency} />
+ const refunds = (result.ok && result.data.order_status === 'paid' ? result.data.refund_details : [])
+ .reduce(mergeRefunds, [])
+ const totalRefunded = refunds.map(r => r.amount).reduce((p, c) => sumPrices(c, p), ':0')
+ const orderPrice = (result.ok && result.data.order_status === 'paid' ? result.data.contract_terms.amount : undefined)
+ const totalRefundable = !orderPrice ? undefined : (refunds.length ? subtractPrices(orderPrice, totalRefunded) : orderPrice)
+
+ const isRefundable = totalRefundable && !AMOUNT_ZERO_REGEX.test(totalRefundable)
+
+ return <ConfirmModal description="refund" danger active onCancel={onCancel} onConfirm={validateAndConfirm}>
+ {refunds.length > 0 && <div class="columns">
+ <div class="column is-2" />
+ <div class="column is-8">
+ <InputGroup name="asd" description={`${totalRefunded} was already refunded`}>
+ <table class="table is-fullwidth">
+ <thead>
+ <tr>
+ <th>date</th>
+ <th>amount</th>
+ <th>reason</th>
+ </tr>
+ </thead>
+ <tbody>
+ {refunds.map(r => {
+ return <tr>
+ <td>{format(new Date(r.timestamp.t_ms), 'yyyy-MM-dd HH:mm:ss')}</td>
+ <td>{r.amount}</td>
+ <td>{r.reason}</td>
+ </tr>
+ })}
+ </tbody>
+ </table>
+ </InputGroup>
+ </div>
+ <div class="column is-2" />
+ </div>}
+
+ { isRefundable && <FormProvider<State> errors={errors} object={form} valueHandler={(d) => setValue(d as any)}>
+ <InputCurrency<State> name="refund" currency={config.currency}>
+ Max refundable: {totalRefundable}
+ </InputCurrency>
<InputSelector name="mainReason" values={['duplicated', 'requested by the customer', 'other']} />
{form.mainReason && <Input<State> name="description" />}
- </FormProvider>
+ </FormProvider> }
+
</ConfirmModal>
}
+
diff --git a/packages/frontend/src/utils/amount.ts b/packages/frontend/src/utils/amount.ts
@@ -0,0 +1,62 @@
+import { MerchantBackend } from "../declaration";
+
+/**
+ * sums two prices,
+ * @param one
+ * @param two
+ * @returns
+ */
+export const sumPrices = (one: string, two: string) => {
+ const [currency, valueOne] = one.split(':')
+ const [, valueTwo] = two.split(':')
+ return `${currency}:${parseInt(valueOne, 10) + parseInt(valueTwo, 10)}`
+}
+
+/**
+ * merge refund with the same description and a difference less than one minute
+ * @param prev list of refunds that will hold the merged refunds
+ * @param cur new refund to add to the list
+ * @returns list with the new refund, may be merged with the last
+ */
+export function mergeRefunds(prev: MerchantBackend.Orders.RefundDetails[], cur: MerchantBackend.Orders.RefundDetails) {
+ let tail;
+
+ if (prev.length === 0 || //empty list
+ cur.timestamp.t_ms === 'never' || //current doesnt have timestamp
+ (tail = prev[prev.length - 1]).timestamp.t_ms === 'never' || // last doesnt have timestamp
+ cur.reason !== tail.reason || //different reason
+ Math.abs(cur.timestamp.t_ms - tail.timestamp.t_ms) > 1000 * 60) {//more than 1 minute difference
+
+ prev.push(cur)
+ return prev
+ }
+
+ prev[prev.length - 1] = {
+ ...tail,
+ amount: sumPrices(tail.amount, cur.amount)
+ }
+
+ return prev
+}
+
+export const multiplyPrice = (price: string, q: number) => {
+ const [currency, value] = price.split(':')
+ const total = parseInt(value, 10) * q
+ return `${currency}:${total}`
+}
+
+export const subtractPrices = (one: string, two: string) => {
+ const [currency, valueOne] = one.split(':')
+ const [, valueTwo] = two.split(':')
+ return `${currency}:${parseInt(valueOne, 10) - parseInt(valueTwo, 10)}`
+}
+
+export 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/utils/constants.ts b/packages/frontend/src/utils/constants.ts
@@ -14,20 +14,22 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
- /**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
- //https://tools.ietf.org/html/rfc8905
-export const PAYTO_REGEX=/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
+//https://tools.ietf.org/html/rfc8905
+export const PAYTO_REGEX = /^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/
-export const AMOUNT_REGEX=/^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
+export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/
export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/
+export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/
+
// how much rows we add every time user hit load more
export const PAGE_SIZE = 20
// how bigger can be the result set
// after this threshold, load more with move the cursor
-export const MAX_RESULT_SIZE = PAGE_SIZE*2-1;
-\ No newline at end of file
+export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
+\ No newline at end of file