Browse Source

fix(admin-ui): Enable retrying of failed refunds

Relates to #873
Michael Bromley 4 years ago
parent
commit
4fc749d020

+ 16 - 16
packages/admin-ui/i18n-coverage.json

@@ -1,54 +1,54 @@
 {
-  "generatedOn": "2021-04-13T22:43:45.241Z",
-  "lastCommit": "58f66ad22105c3706f27c6a965a0db7d99047968",
+  "generatedOn": "2021-05-11T11:36:09.539Z",
+  "lastCommit": "1795f487c8f8f0e146718d31b02fda94fc9a5c4c",
   "translationStatus": {
     "cs": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 751,
       "percentage": 97
     },
     "de": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 729,
       "percentage": 94
     },
     "en": {
-      "tokenCount": 773,
-      "translatedCount": 773,
+      "tokenCount": 775,
+      "translatedCount": 774,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 454,
       "percentage": 59
     },
     "fr": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 688,
       "percentage": 89
     },
     "pl": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 547,
       "percentage": 71
     },
     "pt_BR": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 747,
-      "percentage": 97
+      "percentage": 96
     },
     "ru": {
-      "tokenCount": 773,
-      "translatedCount": 150,
-      "percentage": 19
+      "tokenCount": 775,
+      "translatedCount": 773,
+      "percentage": 100
     },
     "zh_Hans": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 529,
       "percentage": 68
     },
     "zh_Hant": {
-      "tokenCount": 773,
+      "tokenCount": 775,
       "translatedCount": 529,
       "percentage": 68
     }

+ 20 - 2
packages/admin-ui/src/lib/order/src/components/line-refunds/line-refunds.component.ts

@@ -1,5 +1,4 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-
 import { OrderDetail } from '@vendure/admin-ui/core';
 
 @Component({
@@ -10,8 +9,27 @@ import { OrderDetail } from '@vendure/admin-ui/core';
 })
 export class LineRefundsComponent {
     @Input() line: OrderDetail.Lines;
+    @Input() payments: OrderDetail.Payments[];
 
     getRefundedCount(): number {
-        return this.line.items.filter(i => i.refundId != null && !i.cancelled).length;
+        const refunds =
+            this.payments?.reduce(
+                (all, payment) => [...all, ...payment.refunds],
+                [] as OrderDetail.Refunds[],
+            ) ?? [];
+        return this.line.items.filter(i => {
+            if (i.refundId === null && !i.cancelled) {
+                return false;
+            }
+            if (i.refundId) {
+                const refund = refunds.find(r => r.id === i.refundId);
+                if (refund?.state === 'Failed') {
+                    return false;
+                } else {
+                    return true;
+                }
+            }
+            return false;
+        }).length;
     }
 }

+ 8 - 4
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts

@@ -498,9 +498,9 @@ export class OrderDetailComponent
                         return of(undefined);
                     }
 
-                    const operations: Array<Observable<
-                        RefundOrder.RefundOrder | CancelOrder.CancelOrder
-                    >> = [];
+                    const operations: Array<
+                        Observable<RefundOrder.RefundOrder | CancelOrder.CancelOrder>
+                    > = [];
                     if (input.refund.lines.length) {
                         operations.push(
                             this.dataService.order
@@ -527,7 +527,11 @@ export class OrderDetailComponent
                             break;
                         case 'Refund':
                             this.refetchOrder(result).subscribe();
-                            this.notificationService.success(_('order.refund-order-success'));
+                            if (result.state === 'Failed') {
+                                this.notificationService.error(_('order.refund-order-failed'));
+                            } else {
+                                this.notificationService.success(_('order.refund-order-success'));
+                            }
                             break;
                         case 'QuantityTooGreatError':
                         case 'MultipleOrderError':

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html

@@ -56,7 +56,7 @@
                                 [value]="line.quantity"
                                 (input)="updateLineQuantity(line, $event.target.value)"
                             />
-                            <vdr-line-refunds [line]="line"></vdr-line-refunds>
+                            <vdr-line-refunds [line]="line" [payments]="order.payments"></vdr-line-refunds>
                             <vdr-line-fulfillment
                                 [line]="line"
                                 [orderState]="order.state"

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.html

@@ -25,7 +25,7 @@
                 </td>
                 <td class="align-middle quantity">
                     {{ line.quantity }}
-                    <vdr-line-refunds [line]="line"></vdr-line-refunds>
+                    <vdr-line-refunds [line]="line" [payments]="order.payments"></vdr-line-refunds>
                     <vdr-line-fulfillment [line]="line" [orderState]="order.state"></vdr-line-fulfillment>
                 </td>
                 <td class="align-middle total">

+ 15 - 11
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.html

@@ -24,7 +24,7 @@
                 <td class="align-middle sku">{{ line.productVariant.sku }}</td>
                 <td class="align-middle quantity">
                     {{ line.quantity }}
-                    <vdr-line-refunds [line]="line"></vdr-line-refunds>
+                    <vdr-line-refunds [line]="line" [payments]="order.payments"></vdr-line-refunds>
                 </td>
                 <td class="align-middle quantity">
                     {{ line.unitPriceWithTax | localeCurrency: order.currencyCode }}
@@ -41,7 +41,11 @@
                                     <div class="line-promotion" *ngFor="let discount of discounts">
                                         {{ discount.description }}
                                         <div class="promotion-amount">
-                                            {{ discount.amount / 100 / line.quantity | number: '1.0-2' | currency: order.currencyCode }}
+                                            {{
+                                                discount.amount / 100 / line.quantity
+                                                    | number: '1.0-2'
+                                                    | currency: order.currencyCode
+                                            }}
                                         </div>
                                     </div>
                                 </vdr-dropdown-menu>
@@ -140,19 +144,19 @@
                     {{ 'order.refund-total' | translate }}:
                     {{ refundTotal | localeCurrency: order.currencyCode }}
                 </div>
-                <div
-                    class="refund-total-error"
-                    *ngIf="refundTotal < 0 || selectedPayment.amount < refundTotal"
-                >
+                <div class="refund-total-error" *ngIf="refundTotal < 0 || settledPaymentsTotal < refundTotal">
                     {{
                         'order.refund-total-error'
                             | translate
                                 : {
                                       min: 0 | currency: order.currencyCode,
-                                      max: selectedPayment.amount | localeCurrency: order.currencyCode
+                                      max: settledPaymentsTotal | localeCurrency: order.currencyCode
                                   }
                     }}
                 </div>
+                <div class="refund-total-warning" *ngIf="selectedPayment.amount < refundTotal">
+                    {{ 'order.refund-total-warning' | translate }}
+                </div>
             </div>
         </div>
     </div>
@@ -162,10 +166,10 @@
     <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
     <button type="submit" (click)="select()" [disabled]="!canSubmit()" class="btn btn-primary">
         <ng-container *ngIf="isRefunding(); else cancelling">
-        {{
-            'order.refund-with-amount'
-                | translate: { amount: refundTotal | localeCurrency: order.currencyCode }
-        }}
+            {{
+                'order.refund-with-amount'
+                    | translate: { amount: refundTotal | localeCurrency: order.currencyCode }
+            }}
         </ng-container>
         <ng-template #cancelling>
             {{ 'order.cancel-selected-items' | translate }}

+ 4 - 0
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.scss

@@ -40,6 +40,10 @@ clr-checkbox-wrapper {
     .refund-total-error {
         color: var(--color-error-500);
     }
+    .refund-total-warning {
+        color: var(--color-warning-600);
+        max-width: 250px;
+    }
     &.disabled {
         color: var(--color-grey-300);
     }

+ 32 - 3
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.ts

@@ -9,6 +9,7 @@ import {
     OrderLineInput,
     RefundOrderInput,
 } from '@vendure/admin-ui/core';
+import { summate } from '@vendure/common/lib/shared-utils';
 
 type SelectionLine = { quantity: number; refund: boolean; cancel: boolean };
 
@@ -43,8 +44,37 @@ export class RefundOrderDialogComponent
         return itemTotal + (this.refundShipping ? this.order.shippingWithTax : 0) + this.adjustment;
     }
 
+    get settledPaymentsTotal(): number {
+        return this.settledPayments
+            .map(payment => {
+                const paymentTotal = payment.amount;
+                const alreadyRefundedTotal = summate(
+                    payment.refunds.filter(r => r.state !== 'Failed') as Array<Required<OrderDetail.Refunds>>,
+                    'total',
+                );
+                return paymentTotal - alreadyRefundedTotal;
+            })
+            .reduce((sum, amount) => sum + amount, 0);
+    }
+
     lineCanBeRefundedOrCancelled(line: OrderDetail.Lines): boolean {
-        return 0 < line.items.filter(i => i.refundId == null && !i.cancelled).length;
+        const refunds =
+            this.order.payments?.reduce(
+                (all, payment) => [...all, ...payment.refunds],
+                [] as OrderDetail.Refunds[],
+            ) ?? [];
+
+        const refundable = line.items.filter(i => {
+            if (i.cancelled) {
+                return false;
+            }
+            if (i.refundId == null) {
+                return true;
+            }
+            const refund = refunds.find(r => r.id === i.refundId);
+            return refund?.state === 'Failed';
+        });
+        return 0 < refundable.length;
     }
 
     ngOnInit() {
@@ -87,12 +117,11 @@ export class RefundOrderDialogComponent
 
     canSubmit(): boolean {
         if (this.isRefunding()) {
-            // !selectedPayment || !reason || refundTotal === 0 || refundTotal > selectedPayment.amount;
             return !!(
                 this.selectedPayment &&
                 this.reason &&
                 0 < this.refundTotal &&
-                this.refundTotal <= this.selectedPayment.amount
+                this.refundTotal <= this.settledPaymentsTotal
             );
         } else if (this.isCancelling()) {
             return !!this.reason;

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "Důvod refundace/zrušení",
     "refund-cancellation-reason-required": "Uvedení důvodu refundace/zrušení je povinné",
     "refund-metadata": "Data refundace",
+    "refund-order-failed": "",
     "refund-order-success": "Successfully refunded order",
     "refund-reason": "Důvod refundace",
     "refund-reason-customer-request": "Požadavek zákazníka",
@@ -673,6 +674,7 @@
     "refund-shipping": "Refundovat poštovné",
     "refund-total": "Refundovat celkem",
     "refund-total-error": "Částka refundace musí být mezi {min} a {max}",
+    "refund-total-warning": "",
     "refund-with-amount": "Refundovat {amount}",
     "refunded-count": "{count} {count, plural, one {položka} other {položky}} refundovány",
     "removed-items": "Odebrané položky",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "Stornierungs-/Rückzahlungsgrund",
     "refund-cancellation-reason-required": "Stornierungs-/Rückzahlungsgrund ist erforderlich",
     "refund-metadata": "Metadaten zur Rückzahlung",
+    "refund-order-failed": "",
     "refund-order-success": "Bestellung erfolgreich zurückgezahlt",
     "refund-reason": "Rückzahlungsgrund",
     "refund-reason-customer-request": "Kundenanfrage",
@@ -673,6 +674,7 @@
     "refund-shipping": "Versandkosten erstatten",
     "refund-total": "Rückzahlung gesamt",
     "refund-total-error": "Die Erstattungssumme muss zwischen {min} und {max} liegen",
+    "refund-total-warning": "",
     "refund-with-amount": "Rückzahlung {amount}",
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
     "removed-items": "Entfernte Artikel",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "Refund/cancellation reason",
     "refund-cancellation-reason-required": "Refund/cancellation reason is required",
     "refund-metadata": "Refund metadata",
+    "refund-order-failed": "Refund failed",
     "refund-order-success": "Successfully refunded order",
     "refund-reason": "Refund reason",
     "refund-reason-customer-request": "Customer request",
@@ -673,6 +674,7 @@
     "refund-shipping": "Refund shipping",
     "refund-total": "Refund total",
     "refund-total-error": "Refund total must be between {min} and {max}",
+    "refund-total-warning": "Refund total exceeds selected payment amount. The remaining refund amount will be refunded from the other payments.",
     "refund-with-amount": "Refund {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
     "removed-items": "Removed items",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "",
     "refund-cancellation-reason-required": "",
     "refund-metadata": "",
+    "refund-order-failed": "",
     "refund-order-success": "",
     "refund-reason": "",
     "refund-reason-customer-request": "",
@@ -673,6 +674,7 @@
     "refund-shipping": "",
     "refund-total": "",
     "refund-total-error": "",
+    "refund-total-warning": "",
     "refund-with-amount": "",
     "refunded-count": "",
     "removed-items": "",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "",
     "refund-cancellation-reason-required": "",
     "refund-metadata": "Métadonnées de rembousement",
+    "refund-order-failed": "",
     "refund-order-success": "Commande remboursée",
     "refund-reason": "Raison du remboursement",
     "refund-reason-customer-request": "Demande du client",
@@ -673,6 +674,7 @@
     "refund-shipping": "Rembourser la livraison",
     "refund-total": "Remboursement total",
     "refund-total-error": "Le remboursement total doit être entre {min} et {max}",
+    "refund-total-warning": "",
     "refund-with-amount": "Rembourser {amount}",
     "refunded-count": "{count} {count, plural, one {article remboursé} other {articles remboursés}}",
     "removed-items": "",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "Powód zwrotu/anulowania",
     "refund-cancellation-reason-required": "Powód zwrotu/anulowania jest wymagany",
     "refund-metadata": "Metadane zwrotu",
+    "refund-order-failed": "",
     "refund-order-success": "Zamówienie zwrócone pomyślnie",
     "refund-reason": "Powód zwrotu",
     "refund-reason-customer-request": "Prośba klienta",
@@ -673,6 +674,7 @@
     "refund-shipping": "Wysyłka zwrotu",
     "refund-total": "Wartość zwrotu",
     "refund-total-error": "Wartość zwrotu musi być pomiędzy {min} i {max}",
+    "refund-total-warning": "",
     "refund-with-amount": "Zwróć {amount}",
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
     "removed-items": "",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "Motivo do reembolso/cancelamento",
     "refund-cancellation-reason-required": "O motivo do reembolso/cancelamento é obrigatório",
     "refund-metadata": "Dados do reembolso",
+    "refund-order-failed": "",
     "refund-order-success": "Pedido de reembolso efetuado com sucesso",
     "refund-reason": "Motivo do reembolso",
     "refund-reason-customer-request": "Solicitação do cliente",
@@ -673,6 +674,7 @@
     "refund-shipping": "Envio de reembolso",
     "refund-total": "Total do reembolso",
     "refund-total-error": "Total do reembolso deve ser entre {min} e {max}",
+    "refund-total-warning": "",
     "refund-with-amount": "Reembolso {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
     "removed-items": "Itens removidos",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "Причина возврата / отмены",
     "refund-cancellation-reason-required": "Укажите причину возврата / отмены",
     "refund-metadata": "Метаданные возврата",
+    "refund-order-failed": "",
     "refund-order-success": "Заказ успешно возвращен",
     "refund-reason": "Причина возврата",
     "refund-reason-customer-request": "Запрос клиента",
@@ -673,6 +674,7 @@
     "refund-shipping": "Возврат стоимости доставки",
     "refund-total": "Общая сумма возврата",
     "refund-total-error": "Общая сумма возврата должна быть в пределах {min} и {max}",
+    "refund-total-warning": "",
     "refund-with-amount": "Возврат {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} возвращен",
     "removed-items": "Удаленные предметы",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "",
     "refund-cancellation-reason-required": "",
     "refund-metadata": "退款元数据",
+    "refund-order-failed": "",
     "refund-order-success": "退款订单处理成功",
     "refund-reason": "退款原因",
     "refund-reason-customer-request": "客户要求",
@@ -673,6 +674,7 @@
     "refund-shipping": "退运费",
     "refund-total": "退款总计",
     "refund-total-error": "退款总计必须大于{min}并少于{max}之间",
+    "refund-total-warning": "",
     "refund-with-amount": "退款金额{amount}",
     "refunded-count": "{count}个商品已退款",
     "removed-items": "",

+ 2 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -666,6 +666,7 @@
     "refund-cancellation-reason": "",
     "refund-cancellation-reason-required": "",
     "refund-metadata": "退款元數據",
+    "refund-order-failed": "",
     "refund-order-success": "退款訂單處理成功",
     "refund-reason": "退款原因",
     "refund-reason-customer-request": "客户要求",
@@ -673,6 +674,7 @@
     "refund-shipping": "退運費",
     "refund-total": "退款總計",
     "refund-total-error": "退款總計必須大於{min}並少遊{max}之間",
+    "refund-total-warning": "",
     "refund-with-amount": "退款金額{amount}",
     "refunded-count": "{count}個商品已退款",
     "removed-items": "",