gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated: refunded table


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: refunded table
Date: Tue, 13 Apr 2021 22:57:00 +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 2444fb8  refunded table
2444fb8 is described below

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

    refunded table
---
 CHANGELOG.md                                       |  1 -
 .../frontend/src/components/form/InputCurrency.tsx |  4 +-
 .../frontend/src/components/form/InputGroup.tsx    |  5 +-
 .../paths/instance/orders/create/CreatePage.tsx    | 35 +++---------
 .../paths/instance/orders/details/DetailPage.tsx   |  9 ++--
 .../src/paths/instance/orders/list/Table.tsx       | 62 ++++++++++++++++++----
 packages/frontend/src/utils/amount.ts              | 62 ++++++++++++++++++++++
 packages/frontend/src/utils/constants.ts           | 18 ++++---
 8 files changed, 143 insertions(+), 53 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29fcb9f..51e0f45 100644
--- 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
index fa1eba9..f932d34 100644
--- 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
index 5d9e551..e80ef66 100644
--- 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
index d5b4527..46dad20 100644
--- 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
index f631858..85b8d8e 100644
--- 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
index 9538d55..e0646cc 100644
--- 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
new file mode 100644
index 0000000..ac98f0d
--- /dev/null
+++ 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
index 8d642d7..8ca284e 100644
--- 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

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]