Przeglądaj źródła

feat(admin-ui): Allow cancellation of OrderItems without refunding

Relates to #569
Michael Bromley 5 lat temu
rodzic
commit
df55d2dbfa

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

@@ -1,21 +1,21 @@
 {
-  "generatedOn": "2020-12-04T13:17:32.104Z",
-  "lastCommit": "efe640c5011072eba4471e431dc2dc503973802b",
+  "generatedOn": "2020-12-04T16:28:55.124Z",
+  "lastCommit": "8e5b1dd3b6d362fd9431d242e5e829419e6f3179",
   "translationStatus": {
     "cs": {
       "tokenCount": 713,
-      "translatedCount": 688,
+      "translatedCount": 685,
       "percentage": 96
     },
     "de": {
       "tokenCount": 713,
-      "translatedCount": 597,
-      "percentage": 84
+      "translatedCount": 594,
+      "percentage": 83
     },
     "en": {
       "tokenCount": 713,
-      "translatedCount": 708,
-      "percentage": 99
+      "translatedCount": 713,
+      "percentage": 100
     },
     "es": {
       "tokenCount": 713,
@@ -24,22 +24,22 @@
     },
     "pl": {
       "tokenCount": 713,
-      "translatedCount": 552,
+      "translatedCount": 549,
       "percentage": 77
     },
     "pt_BR": {
       "tokenCount": 713,
-      "translatedCount": 643,
+      "translatedCount": 640,
       "percentage": 90
     },
     "zh_Hans": {
       "tokenCount": 713,
-      "translatedCount": 536,
+      "translatedCount": 533,
       "percentage": 75
     },
     "zh_Hant": {
       "tokenCount": 713,
-      "translatedCount": 536,
+      "translatedCount": 533,
       "percentage": 75
     }
   }

+ 29 - 7
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -3568,17 +3568,18 @@ export type OrderItem = Node & {
   createdAt: Scalars['DateTime'];
   updatedAt: Scalars['DateTime'];
   cancelled: Scalars['Boolean'];
+  /** The price of a single unit, excluding tax and discounts */
+  unitPrice: Scalars['Int'];
+  /** The price of a single unit, including tax but excluding discounts */
+  unitPriceWithTax: Scalars['Int'];
   /**
-   * The price of a single unit, excluding tax and discounts
+   * The price of a single unit including discounts, excluding tax.
    * 
    * If Order-level discounts have been applied, this will not be the
-   * actual taxable unit price, but is generally the correct price to display to customers to avoid confusion
+   * actual taxable unit price (see `proratedUnitPrice`), but is generally the
+   * correct price to display to customers to avoid confusion
    * about the internal handling of distributed Order-level discounts.
    */
-  unitPrice: Scalars['Int'];
-  /** The price of a single unit, including tax but excluding discounts */
-  unitPriceWithTax: Scalars['Int'];
-  /** The price of a single unit including discounts, excluding tax */
   discountedUnitPrice: Scalars['Int'];
   /** The price of a single unit including discounts and tax */
   discountedUnitPriceWithTax: Scalars['Int'];
@@ -3607,8 +3608,29 @@ export type OrderLine = Node & {
   updatedAt: Scalars['DateTime'];
   productVariant: ProductVariant;
   featuredAsset?: Maybe<Asset>;
+  /** The price of a single unit, excluding tax and discounts */
   unitPrice: Scalars['Int'];
+  /** The price of a single unit, including tax but excluding discounts */
   unitPriceWithTax: Scalars['Int'];
+  /**
+   * The price of a single unit including discounts, excluding tax.
+   * 
+   * If Order-level discounts have been applied, this will not be the
+   * actual taxable unit price (see `proratedUnitPrice`), but is generally the
+   * correct price to display to customers to avoid confusion
+   * about the internal handling of distributed Order-level discounts.
+   */
+  discountedUnitPrice: Scalars['Int'];
+  /** The price of a single unit including discounts and tax */
+  discountedUnitPriceWithTax: Scalars['Int'];
+  /**
+   * The actual unit price, taking into account both item discounts _and_ prorated (proportially-distributed)
+   * Order-level discounts. This value is the true economic value of the OrderItem, and is used in tax
+   * and refund calculations.
+   */
+  proratedUnitPrice: Scalars['Int'];
+  /** The proratedUnitPrice including tax */
+  proratedUnitPriceWithTax: Scalars['Int'];
   quantity: Scalars['Int'];
   items: Array<OrderItem>;
   /** @deprecated Use `linePriceWithTax` instead */
@@ -5259,7 +5281,7 @@ export type FulfillmentFragment = (
 
 export type OrderLineFragment = (
   { __typename?: 'OrderLine' }
-  & Pick<OrderLine, 'id' | 'unitPrice' | 'unitPriceWithTax' | 'quantity' | 'linePrice' | 'lineTax' | 'linePriceWithTax' | 'discountedLinePrice' | 'discountedLinePriceWithTax'>
+  & Pick<OrderLine, 'id' | 'unitPrice' | 'unitPriceWithTax' | 'proratedUnitPrice' | 'proratedUnitPriceWithTax' | 'quantity' | 'linePrice' | 'lineTax' | 'linePriceWithTax' | 'discountedLinePrice' | 'discountedLinePriceWithTax'>
   & { featuredAsset?: Maybe<(
     { __typename?: 'Asset' }
     & Pick<Asset, 'preview'>

+ 2 - 0
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -89,6 +89,8 @@ export const ORDER_LINE_FRAGMENT = gql`
         }
         unitPrice
         unitPriceWithTax
+        proratedUnitPrice
+        proratedUnitPriceWithTax
         quantity
         items {
             id

+ 32 - 34
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts

@@ -5,6 +5,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     AdjustmentType,
     BaseDetailComponent,
+    CancelOrder,
     CustomFieldConfig,
     DataService,
     EditNoteDialogComponent,
@@ -16,11 +17,11 @@ import {
     Order,
     OrderDetail,
     OrderLineFragment,
+    RefundOrder,
     ServerConfigService,
     SortOrder,
 } from '@vendure/admin-ui/core';
-import { omit } from '@vendure/common/lib/omit';
-import { EMPTY, Observable, of, Subject } from 'rxjs';
+import { EMPTY, merge, Observable, of, Subject } from 'rxjs';
 import { map, mapTo, startWith, switchMap, take } from 'rxjs/operators';
 
 import { CancelOrderDialogComponent } from '../cancel-order-dialog/cancel-order-dialog.component';
@@ -401,46 +402,37 @@ export class OrderDetailComponent
             })
             .pipe(
                 switchMap(input => {
-                    if (input) {
-                        return this.dataService.order.refundOrder(omit(input, ['cancel'])).pipe(
-                            switchMap(({ refundOrder }) => {
-                                switch (refundOrder.__typename) {
-                                    case 'Refund':
-                                        if (input.cancel.length) {
-                                            return this.dataService.order
-                                                .cancelOrder({
-                                                    orderId: this.id,
-                                                    lines: input.cancel,
-                                                    reason: input.reason,
-                                                })
-                                                .pipe(map(({ cancelOrder }) => cancelOrder));
-                                        } else {
-                                            return of(refundOrder);
-                                        }
-                                    case 'AlreadyRefundedError':
-                                    case 'OrderStateTransitionError':
-                                    case 'MultipleOrderError':
-                                    case 'NothingToRefundError':
-                                    case 'PaymentOrderMismatchError':
-                                    case 'QuantityTooGreatError':
-                                    case 'RefundOrderStateError':
-                                    case 'RefundStateTransitionError':
-                                        this.notificationService.error(refundOrder.message);
-                                    // tslint:disable-next-line:no-switch-case-fall-through
-                                    default:
-                                        return of(undefined);
-                                }
-                            }),
-                        );
-                    } else {
+                    if (!input) {
                         return of(undefined);
                     }
+
+                    const operations: Array<Observable<
+                        RefundOrder.RefundOrder | CancelOrder.CancelOrder
+                    >> = [];
+                    if (input.refund.lines.length) {
+                        operations.push(
+                            this.dataService.order
+                                .refundOrder(input.refund)
+                                .pipe(map(res => res.refundOrder)),
+                        );
+                    }
+                    if (input.cancel.lines?.length) {
+                        operations.push(
+                            this.dataService.order
+                                .cancelOrder(input.cancel)
+                                .pipe(map(res => res.cancelOrder)),
+                        );
+                    }
+                    return merge(...operations);
                 }),
             )
             .subscribe(result => {
                 if (result) {
                     switch (result.__typename) {
                         case 'Order':
+                            this.refetchOrder(result).subscribe();
+                            this.notificationService.success(_('order.cancelled-order-success'));
+                            break;
                         case 'Refund':
                             this.refetchOrder(result).subscribe();
                             this.notificationService.success(_('order.refund-order-success'));
@@ -450,7 +442,13 @@ export class OrderDetailComponent
                         case 'OrderStateTransitionError':
                         case 'CancelActiveOrderError':
                         case 'EmptyOrderLineSelectionError':
+                        case 'AlreadyRefundedError':
+                        case 'NothingToRefundError':
+                        case 'PaymentOrderMismatchError':
+                        case 'RefundOrderStateError':
+                        case 'RefundStateTransitionError':
                             this.notificationService.error(result.message);
+                            break;
                     }
                 }
             });

+ 113 - 69
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.html

@@ -1,6 +1,6 @@
-<ng-template vdrDialogTitle>{{ 'order.refund-order' | translate }}</ng-template>
+<ng-template vdrDialogTitle>{{ 'order.refund-and-cancel-order' | translate }}</ng-template>
 
-<div class="fulfillment-wrapper">
+<div class="refund-wrapper">
     <div class="order-lines">
         <table class="table">
             <thead>
@@ -10,13 +10,15 @@
                     <th>{{ 'order.product-sku' | translate }}</th>
                     <th>{{ 'order.quantity' | translate }}</th>
                     <th>{{ 'order.unit-price' | translate }}</th>
+                    <th>{{ 'order.prorated-unit-price' | translate }}</th>
+                    <th>{{ 'order.quantity' | translate }}</th>
                     <th>{{ 'order.refund' | translate }}</th>
-                    <th>{{ 'order.return-to-stock' | translate }}</th>
+                    <th>{{ 'order.cancel' | translate }}</th>
                 </tr>
             </thead>
             <tr *ngFor="let line of order.lines" class="order-line">
                 <td class="align-middle thumb">
-                    <img [src]="line.featuredAsset | assetPreview:'tiny'" />
+                    <img [src]="line.featuredAsset | assetPreview: 'tiny'" />
                 </td>
                 <td class="align-middle name">{{ line.productVariant.name }}</td>
                 <td class="align-middle sku">{{ line.productVariant.sku }}</td>
@@ -27,20 +29,52 @@
                 <td class="align-middle quantity">
                     {{ line.unitPriceWithTax / 100 | currency: order.currencyCode }}
                 </td>
+                <td class="align-middle quantity">
+                    <div class="prorated-wrapper">
+                        {{ line.proratedUnitPriceWithTax / 100 | currency: order.currencyCode }}
+                        <ng-container *ngIf="line.discounts as discounts">
+                            <vdr-dropdown *ngIf="discounts.length">
+                                <div class="promotions-label" vdrDropdownTrigger>
+                                    <button class="icon-button"><clr-icon shape="info"></clr-icon></button>
+                                </div>
+                                <vdr-dropdown-menu>
+                                    <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 }}
+                                        </div>
+                                    </div>
+                                </vdr-dropdown-menu>
+                            </vdr-dropdown>
+                        </ng-container>
+                    </div>
+                </td>
                 <td class="align-middle fulfil">
                     <input
-                        *ngIf="lineCanBeRefunded(line)"
+                        *ngIf="lineCanBeRefundedOrCancelled(line)"
                         [(ngModel)]="lineQuantities[line.id].quantity"
                         type="number"
                         [max]="line.quantity"
                         min="0"
+                        (input)="handleZeroQuantity(lineQuantities[line.id])"
                     />
                 </td>
                 <td class="align-middle">
                     <div class="cancel-checkbox-wrapper">
                         <input
                             type="checkbox"
-                            *ngIf="lineCanBeRefunded(line)"
+                            *ngIf="lineCanBeRefundedOrCancelled(line)"
+                            clrCheckbox
+                            [disabled]="0 === lineQuantities[line.id].quantity"
+                            [(ngModel)]="lineQuantities[line.id].refund"
+                        />
+                    </div>
+                </td>
+                <td class="align-middle">
+                    <div class="cancel-checkbox-wrapper">
+                        <input
+                            type="checkbox"
+                            *ngIf="lineCanBeRefundedOrCancelled(line)"
                             clrCheckbox
                             [disabled]="0 === lineQuantities[line.id].quantity"
                             [(ngModel)]="lineQuantities[line.id].cancel"
@@ -50,65 +84,75 @@
             </tr>
         </table>
     </div>
-    <div class="refund-details">
-        <label class="clr-control-label">{{ 'order.refund-reason' | translate }}</label>
-        <ng-select
-            [items]="reasons"
-            bindLabel="name"
-            autofocus
-            [placeholder]="'order.refund-reason-required' | translate"
-            bindValue="id"
-            [addTag]="true"
-            [(ngModel)]="reason"
-        ></ng-select>
+    <div class="refund-details mt4">
+        <div>
+            <label class="clr-control-label">{{ 'order.refund-cancellation-reason' | translate }}</label>
+            <ng-select
+                [disabled]="!isRefunding() && !isCancelling()"
+                [items]="reasons"
+                bindLabel="name"
+                autofocus
+                [placeholder]="'order.refund-cancellation-reason-required' | translate"
+                bindValue="id"
+                [addTag]="true"
+                [(ngModel)]="reason"
+            ></ng-select>
+        </div>
 
-        <clr-select-container>
-            <label>{{ 'order.payment-to-refund' | translate }}</label>
-            <select clrSelect name="options" [(ngModel)]="selectedPayment">
-                <option
-                    *ngFor="let payment of settledPayments"
-                    [ngValue]="payment"
-                    [disabled]="payment.state !== 'Settled'"
-                >
-                    #{{ payment.id }} {{ payment.method }}:
-                    {{ payment.amount / 100 | currency: order.currencyCode }}
-                </option>
-            </select>
-        </clr-select-container>
+        <div>
+            <clr-select-container>
+                <label>{{ 'order.payment-to-refund' | translate }}</label>
+                <select clrSelect name="options" [(ngModel)]="selectedPayment" [disabled]="!isRefunding()">
+                    <option
+                        *ngFor="let payment of settledPayments"
+                        [ngValue]="payment"
+                        [disabled]="payment.state !== 'Settled'"
+                    >
+                        #{{ payment.id }} {{ payment.method }}:
+                        {{ payment.amount / 100 | currency: order.currencyCode }}
+                    </option>
+                </select>
+            </clr-select-container>
 
-        <clr-checkbox-wrapper>
-            <input type="checkbox" clrCheckbox [(ngModel)]="refundShipping" />
-            <label>
-                {{ 'order.refund-shipping' | translate }} ({{
-                    order.shipping / 100 | currency: order.currencyCode
-                }})
-            </label>
-        </clr-checkbox-wrapper>
-        <clr-input-container>
-            <label>{{ 'order.refund-adjustment' | translate }}</label>
-            <vdr-currency-input
-                clrInput
-                [currencyCode]="order.currencyCode"
-                [(ngModel)]="adjustment"
-            ></vdr-currency-input>
-        </clr-input-container>
-        <div class="totals">
-            <div class="order-total">
-                {{ 'order.payment-amount' | translate }}:
-                {{ selectedPayment.amount / 100 | currency: order.currencyCode }}
-            </div>
-            <div class="refund-total">
-                {{ 'order.refund-total' | translate }}: {{ refundTotal / 100 | currency: order.currencyCode }}
-            </div>
-            <div class="refund-total-error" *ngIf="refundTotal < 0 || selectedPayment.amount < refundTotal">
-                {{
-                    'order.refund-total-error'
-                        | translate
-                            : {
-                                  min: 0 | currency: order.currencyCode,
-                                  max: selectedPayment.amount / 100 | currency: order.currencyCode
-                              }
-                }}
+            <clr-checkbox-wrapper>
+                <input type="checkbox" clrCheckbox [(ngModel)]="refundShipping" [disabled]="!isRefunding()" />
+                <label>
+                    {{ 'order.refund-shipping' | translate }} ({{
+                        order.shipping / 100 | currency: order.currencyCode
+                    }})
+                </label>
+            </clr-checkbox-wrapper>
+            <clr-input-container>
+                <label>{{ 'order.refund-adjustment' | translate }}</label>
+                <vdr-currency-input
+                    clrInput
+                    [disabled]="!isRefunding()"
+                    [currencyCode]="order.currencyCode"
+                    [(ngModel)]="adjustment"
+                ></vdr-currency-input>
+            </clr-input-container>
+            <div class="totals" [class.disabled]="!isRefunding()">
+                <div class="order-total">
+                    {{ 'order.payment-amount' | translate }}:
+                    {{ selectedPayment.amount / 100 | currency: order.currencyCode }}
+                </div>
+                <div class="refund-total">
+                    {{ 'order.refund-total' | translate }}:
+                    {{ refundTotal / 100 | currency: order.currencyCode }}
+                </div>
+                <div
+                    class="refund-total-error"
+                    *ngIf="refundTotal < 0 || selectedPayment.amount < refundTotal"
+                >
+                    {{
+                        'order.refund-total-error'
+                            | translate
+                                : {
+                                      min: 0 | currency: order.currencyCode,
+                                      max: selectedPayment.amount / 100 | currency: order.currencyCode
+                                  }
+                    }}
+                </div>
             </div>
         </div>
     </div>
@@ -116,15 +160,15 @@
 
 <ng-template vdrDialogButtons>
     <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
-    <button
-        type="submit"
-        (click)="select()"
-        [disabled]="!selectedPayment || !reason || refundTotal === 0 || refundTotal > selectedPayment.amount"
-        class="btn btn-primary"
-    >
+    <button type="submit" (click)="select()" [disabled]="!canSubmit()" class="btn btn-primary">
+        <ng-container *ngIf="isRefunding(); else cancelling">
         {{
             'order.refund-with-amount'
                 | translate: { amount: refundTotal / 100 | currency: order.currencyCode }
         }}
+        </ng-container>
+        <ng-template #cancelling>
+            {{ 'order.cancel-selected-items' | translate }}
+        </ng-template>
     </button>
 </ng-template>

+ 22 - 16
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.scss

@@ -5,23 +5,9 @@
     display: flex;
     min-height: 64vh;
 }
-.fulfillment-wrapper {
+.refund-wrapper {
     flex: 1;
-    @media screen and (min-width: $breakpoint-small) {
-        display: flex;
-        flex-direction: row;
-    }
-
-    .refund-details {
-        @media screen and (min-width: $breakpoint-small) {
-            margin-top: 0;
-            margin-left: 24px;
-            width: 250px;
-        }
-        clr-select-container {
-            margin: 12px 0;
-        }
-    }
+    flex-direction: column;
     .order-lines {
         flex: 1;
         overflow-y: auto;
@@ -43,6 +29,10 @@ clr-checkbox-wrapper {
     margin-bottom: 12px;
     display: block;
 }
+.refund-details {
+    display: flex;
+    justify-content: space-between;
+}
 .totals {
     margin-top: 48px;
     .refund-total {
@@ -51,4 +41,20 @@ clr-checkbox-wrapper {
     .refund-total-error {
         color: $color-error-500;
     }
+    &.disabled {
+        color: $color-grey-300;
+    }
+}
+.prorated-wrapper {
+    display: flex;
+    justify-content: center;
+}
+.line-promotion {
+    display: flex;
+    justify-content: space-between;
+    font-size: 12px;
+    padding: 3px 6px;
+    .promotion-amount {
+        margin-left: 12px;
+    }
 }

+ 80 - 25
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.ts

@@ -1,6 +1,16 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { Dialog, I18nService, OrderDetail, OrderDetailFragment, OrderLineInput, RefundOrderInput } from '@vendure/admin-ui/core';
+import {
+    CancelOrderInput,
+    Dialog,
+    I18nService,
+    OrderDetail,
+    OrderDetailFragment,
+    OrderLineInput,
+    RefundOrderInput,
+} from '@vendure/admin-ui/core';
+
+type SelectionLine = { quantity: number; refund: boolean; cancel: boolean };
 
 @Component({
     selector: 'vdr-refund-order-dialog',
@@ -9,13 +19,13 @@ import { Dialog, I18nService, OrderDetail, OrderDetailFragment, OrderLineInput,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class RefundOrderDialogComponent
-    implements OnInit, Dialog<RefundOrderInput & { cancel: OrderLineInput[] }> {
+    implements OnInit, Dialog<{ cancel: CancelOrderInput; refund: RefundOrderInput }> {
     order: OrderDetailFragment;
-    resolveWith: (result?: RefundOrderInput & { cancel: OrderLineInput[] }) => void;
+    resolveWith: (result?: { cancel: CancelOrderInput; refund: RefundOrderInput }) => void;
     reason: string;
     settledPayments: OrderDetail.Payments[];
     selectedPayment: OrderDetail.Payments;
-    lineQuantities: { [lineId: string]: { quantity: number; cancel: boolean } } = {};
+    lineQuantities: { [lineId: string]: SelectionLine } = {};
     refundShipping = false;
     adjustment = 0;
     reasons: string[] = [_('order.refund-reason-customer-request'), _('order.refund-reason-not-available')];
@@ -26,12 +36,14 @@ export class RefundOrderDialogComponent
 
     get refundTotal(): number {
         const itemTotal = this.order.lines.reduce((total, line) => {
-            return total + line.unitPriceWithTax * (this.lineQuantities[line.id].quantity || 0);
+            const lineRef = this.lineQuantities[line.id];
+            const refundCount = (lineRef.refund && lineRef.quantity) || 0;
+            return total + line.proratedUnitPriceWithTax * refundCount;
         }, 0);
-        return itemTotal + (this.refundShipping ? this.order.shipping : 0) + this.adjustment;
+        return itemTotal + (this.refundShipping ? this.order.shippingWithTax : 0) + this.adjustment;
     }
 
-    lineCanBeRefunded(line: OrderDetail.Lines): boolean {
+    lineCanBeRefundedOrCancelled(line: OrderDetail.Lines): boolean {
         return 0 < line.items.filter(i => i.refundId == null && !i.cancelled).length;
     }
 
@@ -41,6 +53,7 @@ export class RefundOrderDialogComponent
                 ...result,
                 [line.id]: {
                     quantity: 0,
+                    refund: false,
                     cancel: false,
                 },
             };
@@ -51,28 +64,61 @@ export class RefundOrderDialogComponent
         }
     }
 
+    handleZeroQuantity(line?: SelectionLine) {
+        if (line?.quantity === 0) {
+            line.cancel = false;
+            line.refund = false;
+        }
+    }
+
+    isRefunding(): boolean {
+        const result = Object.values(this.lineQuantities).reduce((isRefunding, line) => {
+            return isRefunding || (0 < line.quantity && line.refund);
+        }, false);
+        return result;
+    }
+
+    isCancelling(): boolean {
+        const result = Object.values(this.lineQuantities).reduce((isCancelling, line) => {
+            return isCancelling || (0 < line.quantity && line.cancel);
+        }, false);
+        return result;
+    }
+
+    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
+            );
+        } else if (this.isCancelling()) {
+            return !!this.reason;
+        }
+        return false;
+    }
+
     select() {
         const payment = this.selectedPayment;
         if (payment) {
-            const lines = Object.entries(this.lineQuantities)
-                .map(([orderLineId, data]) => ({
-                    orderLineId,
-                    quantity: data.quantity,
-                }))
-                .filter(l => 0 < l.quantity);
-            const cancel = Object.entries(this.lineQuantities)
-                .filter(([orderLineId, data]) => data.cancel)
-                .map(([orderLineId, data]) => ({
-                    orderLineId,
-                    quantity: data.quantity,
-                }));
+            const refundLines = this.getOrderLineInput(line => line.refund);
+            const cancelLines = this.getOrderLineInput(line => line.cancel);
+
             this.resolveWith({
-                lines,
-                cancel,
-                reason: this.reason,
-                shipping: this.refundShipping ? this.order.shipping : 0,
-                adjustment: this.adjustment,
-                paymentId: payment.id,
+                refund: {
+                    lines: refundLines,
+                    reason: this.reason,
+                    shipping: this.refundShipping ? this.order.shipping : 0,
+                    adjustment: this.adjustment,
+                    paymentId: payment.id,
+                },
+                cancel: {
+                    lines: cancelLines,
+                    orderId: this.order.id,
+                    reason: this.reason,
+                },
             });
         }
     }
@@ -80,4 +126,13 @@ export class RefundOrderDialogComponent
     cancel() {
         this.resolveWith();
     }
+
+    private getOrderLineInput(filterFn: (line: SelectionLine) => boolean): OrderLineInput[] {
+        return Object.entries(this.lineQuantities)
+            .filter(([orderLineId, line]) => 0 < line.quantity && filterFn(line))
+            .map(([orderLineId, line]) => ({
+                orderLineId,
+                quantity: line.quantity,
+            }));
+    }
 }

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

@@ -610,23 +610,23 @@
     "product-name": "Název produktu",
     "product-sku": "SKU",
     "promotions-applied": "Promotions applied",
+    "prorated-unit-price": "",
     "quantity": "Množství",
     "refund": "Refundace",
     "refund-adjustment": "Úprava",
     "refund-and-cancel-order": "Refundovat a zrušit objednávku",
+    "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": "Refundace objednávky",
     "refund-order-success": "Successfully refunded order",
     "refund-reason": "Důvod refundace",
     "refund-reason-customer-request": "Požadavek zákazníka",
     "refund-reason-not-available": "Neuveden",
-    "refund-reason-required": "Uvedení důvodu refundace je povinné",
     "refund-shipping": "Refundovat poštovné",
     "refund-total": "Refundovat celkem",
     "refund-total-error": "Částka refundace musí být mezi {min} a {max}",
     "refund-with-amount": "Refundovat {amount}",
     "refunded-count": "{count} {count, plural, one {položka} other {položky}} refundovány",
-    "return-to-stock": "Vrátit do skladu",
     "search-by-order-code": "Hledat na základě kódu objednávky",
     "set-fulfillment-state": "Označit jako {state}",
     "settle-payment": "Vypořádání platby",
@@ -746,4 +746,4 @@
     "job-result": "Výsledek úlohy",
     "job-state": "Stav úlohy"
   }
-}
+}

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

@@ -610,23 +610,23 @@
     "product-name": "Produktname",
     "product-sku": "Artikelnummer",
     "promotions-applied": "Aktivierte Werbeaktionen",
+    "prorated-unit-price": "",
     "quantity": "Anzahl",
     "refund": "Rückzahlung",
     "refund-adjustment": "Anpassung",
     "refund-and-cancel-order": "Rückzahlung & Stornierung der Bestellung",
+    "refund-cancellation-reason": "Stornierungs-/Rückzahlungsgrund",
+    "refund-cancellation-reason-required": "Stornierungs-/Rückzahlungsgrund ist erforderlich",
     "refund-metadata": "Metadaten zur Rückzahlung",
-    "refund-order": "Bestellung zurückzahlen",
     "refund-order-success": "Bestellung erfolgreich zurückgezahlt",
     "refund-reason": "Rückzahlungsgrund",
     "refund-reason-customer-request": "Kundenanfrage",
     "refund-reason-not-available": "Nicht verfügbar",
-    "refund-reason-required": "Rückzahlungsgrund ist erforderlich",
     "refund-shipping": "Versandkosten erstatten",
     "refund-total": "Rückzahlung gesamt",
     "refund-total-error": "Die Erstattungssumme muss zwischen {min} und {max} liegen",
     "refund-with-amount": "Rückzahlung {amount}",
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
-    "return-to-stock": "Zum Lagerbestand hinzufügen",
     "search-by-order-code": "Suche nach Bestellcode",
     "set-fulfillment-state": "",
     "settle-payment": "Zahlung durchführen",
@@ -746,4 +746,4 @@
     "job-result": "Job-Ergebnis",
     "job-state": "Job-Status"
   }
-}
+}

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

@@ -610,23 +610,23 @@
     "product-name": "Product name",
     "product-sku": "SKU",
     "promotions-applied": "Promotions applied",
+    "prorated-unit-price": "Prorated unit price",
     "quantity": "Quantity",
     "refund": "Refund",
     "refund-adjustment": "Adjustment",
     "refund-and-cancel-order": "Refund & cancel order",
+    "refund-cancellation-reason": "Refund/cancellation reason",
+    "refund-cancellation-reason-required": "Refund/cancellation reason is required",
     "refund-metadata": "Refund metadata",
-    "refund-order": "Refund order",
     "refund-order-success": "Successfully refunded order",
     "refund-reason": "Refund reason",
     "refund-reason-customer-request": "Customer request",
     "refund-reason-not-available": "Not available",
-    "refund-reason-required": "Refund reason is required",
     "refund-shipping": "Refund shipping",
     "refund-total": "Refund total",
     "refund-total-error": "Refund total must be between {min} and {max}",
     "refund-with-amount": "Refund {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
-    "return-to-stock": "Return to stock",
     "search-by-order-code": "Search by order code",
     "set-fulfillment-state": "Mark as {state}",
     "settle-payment": "Settle payment",
@@ -678,7 +678,7 @@
     "email-address": "Email address",
     "filter-by-member-name": "Filter by country",
     "first-name": "First name",
-    "fulfillment-handler": "",
+    "fulfillment-handler": "Fulfillment handler",
     "global-out-of-stock-threshold": "Global out-of-stock threshold",
     "global-out-of-stock-threshold-tooltip": "Sets the stock level at which this a variant is considered to be out of stock. Using a negative value enables backorder support. Can be overridden by product variants.",
     "last-name": "Last name",
@@ -746,4 +746,4 @@
     "job-result": "Job result",
     "job-state": "Job state"
   }
-}
+}

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

@@ -610,23 +610,23 @@
     "product-name": "Nombre del producto",
     "product-sku": "SKU",
     "promotions-applied": "",
+    "prorated-unit-price": "",
     "quantity": "Cantidad",
     "refund": "",
     "refund-adjustment": "",
     "refund-and-cancel-order": "",
+    "refund-cancellation-reason": "",
+    "refund-cancellation-reason-required": "",
     "refund-metadata": "",
-    "refund-order": "",
     "refund-order-success": "",
     "refund-reason": "",
     "refund-reason-customer-request": "",
     "refund-reason-not-available": "",
-    "refund-reason-required": "",
     "refund-shipping": "",
     "refund-total": "",
     "refund-total-error": "",
     "refund-with-amount": "",
     "refunded-count": "",
-    "return-to-stock": "",
     "search-by-order-code": "",
     "set-fulfillment-state": "",
     "settle-payment": "",

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

@@ -610,23 +610,23 @@
     "product-name": "Nazwa produktu",
     "product-sku": "SKU",
     "promotions-applied": "Zastosowane promocje",
+    "prorated-unit-price": "",
     "quantity": "Ilość",
     "refund": "Zwrot",
     "refund-adjustment": "Regulacja",
     "refund-and-cancel-order": "Zwróć i anuluj zamówienie",
+    "refund-cancellation-reason": "Powód zwrotu/anulowania",
+    "refund-cancellation-reason-required": "Powód zwrotu/anulowania jest wymagany",
     "refund-metadata": "Metadane zwrotu",
-    "refund-order": "Zwrót zamówienie",
     "refund-order-success": "Zamówienie zwrócone pomyślnie",
     "refund-reason": "Powód zwrotu",
     "refund-reason-customer-request": "Prośba klienta",
     "refund-reason-not-available": "Niedostępny",
-    "refund-reason-required": "Powód zwrotu jest wymagany",
     "refund-shipping": "Wysyłka zwrotu",
     "refund-total": "Wartość zwrotu",
     "refund-total-error": "Wartość zwrotu musi być pomiędzy {min} i {max}",
     "refund-with-amount": "Zwróć {amount}",
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
-    "return-to-stock": "Zwróć do magazynu",
     "search-by-order-code": "Szukaj po numerze zamówienia",
     "set-fulfillment-state": "",
     "settle-payment": "Rozlicz płatność",
@@ -746,4 +746,4 @@
     "job-result": "Rezultat zlecenia",
     "job-state": "Status zlecenia"
   }
-}
+}

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

@@ -610,23 +610,23 @@
     "product-name": "Nome do produto",
     "product-sku": "SKU",
     "promotions-applied": "Promoções aplicadas",
+    "prorated-unit-price": "",
     "quantity": "Quantidade",
     "refund": "Reembolso",
     "refund-adjustment": "Ajuste",
     "refund-and-cancel-order": "Reembolso & cancelamento do pedido",
+    "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": "Pedido de reembolso",
     "refund-order-success": "Pedido de reembolso efetuado com sucesso",
     "refund-reason": "Motivo do reembolso",
     "refund-reason-customer-request": "Solicitação do cliente",
     "refund-reason-not-available": "Não disponível",
-    "refund-reason-required": "O motivo do reembolso é obrigatório",
     "refund-shipping": "Envio de reembolso",
     "refund-total": "Total do reembolso",
     "refund-total-error": "Total do reembolso deve ser entre {min} e {max}",
     "refund-with-amount": "Reembolso {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
-    "return-to-stock": "Retorno ao estoque",
     "search-by-order-code": "Buscar por código do pedido",
     "set-fulfillment-state": "",
     "settle-payment": "Liquidar pagamento",
@@ -746,4 +746,4 @@
     "job-result": "Resultado do trabalho",
     "job-state": "Estado do trabalho"
   }
-}
+}

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

@@ -610,23 +610,23 @@
     "product-name": "产品名称",
     "product-sku": "库存编码",
     "promotions-applied": "已使用代金券",
+    "prorated-unit-price": "",
     "quantity": "数量",
     "refund": "退款",
     "refund-adjustment": "退款调整",
     "refund-and-cancel-order": "退款|取消订单",
+    "refund-cancellation-reason": "",
+    "refund-cancellation-reason-required": "",
     "refund-metadata": "退款元数据",
-    "refund-order": "退款订单",
     "refund-order-success": "退款订单处理成功",
     "refund-reason": "退款原因",
     "refund-reason-customer-request": "客户要求",
     "refund-reason-not-available": "无库存",
-    "refund-reason-required": "退款原因不能为空",
     "refund-shipping": "退运费",
     "refund-total": "退款总计",
     "refund-total-error": "退款总计必须大于{min}并少于{max}之间",
     "refund-with-amount": "退款金额{amount}",
     "refunded-count": "{count}个商品已退款",
-    "return-to-stock": "返回商品至库存",
     "search-by-order-code": "输入要搜索的订单编号",
     "set-fulfillment-state": "",
     "settle-payment": "结算付款",

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

@@ -610,23 +610,23 @@
     "product-name": "產品名稱",
     "product-sku": "庫存編碼",
     "promotions-applied": "已使用優惠券",
+    "prorated-unit-price": "",
     "quantity": "數量",
     "refund": "退款",
     "refund-adjustment": "退款調整",
     "refund-and-cancel-order": "退款|取消訂單",
+    "refund-cancellation-reason": "",
+    "refund-cancellation-reason-required": "",
     "refund-metadata": "退款元數據",
-    "refund-order": "退款訂單",
     "refund-order-success": "退款訂單處理成功",
     "refund-reason": "退款原因",
     "refund-reason-customer-request": "客户要求",
     "refund-reason-not-available": "無庫存",
-    "refund-reason-required": "退款原因不能為空",
     "refund-shipping": "退運費",
     "refund-total": "退款總計",
     "refund-total-error": "退款總計必須大於{min}並少遊{max}之間",
     "refund-with-amount": "退款金額{amount}",
     "refunded-count": "{count}個商品已退款",
-    "return-to-stock": "返回商品至庫存",
     "search-by-order-code": "輸入要搜索的訂單編號",
     "set-fulfillment-state": "",
     "settle-payment": "結算付款",