merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 2444fb8a6d3160fe2ed639207a7856f026913fd4
parent fb5a844be86c40e0fea2985092724e69f7a39374
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 13 Apr 2021 17:56:43 -0300

refunded table

Diffstat:
MCHANGELOG.md | 1-
Mpackages/frontend/src/components/form/InputCurrency.tsx | 4+++-
Mpackages/frontend/src/components/form/InputGroup.tsx | 5+++--
Mpackages/frontend/src/paths/instance/orders/create/CreatePage.tsx | 35++++++++---------------------------
Mpackages/frontend/src/paths/instance/orders/details/DetailPage.tsx | 9+++++----
Mpackages/frontend/src/paths/instance/orders/list/Table.tsx | 62++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Apackages/frontend/src/utils/amount.ts | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/utils/constants.ts | 20+++++++++++---------
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