Parcourir la source

feat(admin-ui): Enable multiple refunds on an order modification

Relates to #2393
Michael Bromley il y a 2 ans
Parent
commit
9b3aa65faf
14 fichiers modifiés avec 359 ajouts et 156 suppressions
  1. 6 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 33 0
      packages/admin-ui/src/lib/order/src/common/modify-order-types.ts
  3. 24 54
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html
  4. 34 67
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts
  5. 70 25
      packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.html
  6. 15 0
      packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.scss
  7. 64 9
      packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.ts
  8. 0 0
      packages/admin-ui/src/lib/order/src/components/order-modification-summary/order-modification-summary.component.css
  9. 48 0
      packages/admin-ui/src/lib/order/src/components/order-modification-summary/order-modification-summary.component.html
  10. 57 0
      packages/admin-ui/src/lib/order/src/components/order-modification-summary/order-modification-summary.component.ts
  11. 3 0
      packages/admin-ui/src/lib/order/src/components/payment-for-refund-selector/payment-for-refund-selector.component.scss
  12. 1 1
      packages/admin-ui/src/lib/order/src/components/payment-for-refund-selector/payment-for-refund-selector.component.ts
  13. 2 0
      packages/admin-ui/src/lib/order/src/order.module.ts
  14. 2 0
      packages/admin-ui/src/lib/order/src/public_api.ts

+ 6 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -137,6 +137,7 @@ export type AdministratorPaymentInput = {
 };
 
 export type AdministratorRefundInput = {
+  amount: Scalars['Money']['input'];
   paymentId: Scalars['ID']['input'];
   reason?: InputMaybe<Scalars['String']['input']>;
 };
@@ -2570,7 +2571,12 @@ export type ModifyOrderInput = {
   note?: InputMaybe<Scalars['String']['input']>;
   options?: InputMaybe<ModifyOrderOptions>;
   orderId: Scalars['ID']['input'];
+  /**
+   * Deprecated in v2.2.0. Use `refunds` instead to allow multiple refunds to be
+   * applied in the case that multiple payment methods have been used on the order.
+   */
   refund?: InputMaybe<AdministratorRefundInput>;
+  refunds?: InputMaybe<Array<AdministratorRefundInput>>;
   surcharges?: InputMaybe<Array<SurchargeInput>>;
   updateBillingAddress?: InputMaybe<UpdateOrderAddressInput>;
   updateShippingAddress?: InputMaybe<UpdateOrderAddressInput>;

+ 33 - 0
packages/admin-ui/src/lib/order/src/common/modify-order-types.ts

@@ -0,0 +1,33 @@
+import {
+    AddItemInput,
+    CurrencyCode,
+    ModifyOrderInput,
+    OrderDetailFragment,
+    OrderLineInput,
+} from '@vendure/admin-ui/core';
+import { ProductSelectorItem } from '../components/order-editor/order-editor.component';
+
+export interface OrderSnapshot {
+    totalWithTax: number;
+    currencyCode: CurrencyCode;
+    couponCodes: string[];
+    lines: OrderDetailFragment['lines'];
+}
+
+export interface AddedLine {
+    id: string;
+    featuredAsset?: ProductSelectorItem['productAsset'] | null;
+    productVariant: {
+        id: string;
+        name: string;
+        sku: string;
+    };
+    unitPrice: number;
+    unitPriceWithTax: number;
+    quantity: number;
+}
+
+export type ModifyOrderData = Omit<ModifyOrderInput, 'addItems' | 'adjustOrderLines'> & {
+    addItems: Array<AddItemInput & { customFields?: any }>;
+    adjustOrderLines: Array<OrderLineInput & { customFields?: any }>;
+};

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

@@ -12,72 +12,42 @@
     </vdr-action-bar>
 </vdr-page-block>
 
-<vdr-page-detail-layout *ngIf="entity$ | async as order">
+<vdr-page-detail-layout *ngIf="entity as order">
     <vdr-page-detail-sidebar>
         <vdr-card [title]="'order.modification-summary' | translate">
-            <vdr-labeled-data
-                *ngIf="modifyOrderInput.adjustOrderLines?.length"
-                [label]="
-                    'order.modification-adjusting-lines'
-                        | translate : { count: modifyOrderInput.adjustOrderLines?.length }
-                "
-            >
-                <div *ngFor="let line of adjustedLines" class="mb-1">
-                    {{ line }}
-                </div>
-            </vdr-labeled-data>
-            <vdr-labeled-data
-                *ngIf="modifyOrderInput.addItems?.length"
-                [label]="
-                    'order.modification-adding-items'
-                        | translate : { count: modifyOrderInput.addItems?.length }
-                "
-            >
-                <div *ngFor="let item of addedLines">
-                    {{ item.productVariant.name }} x {{ item.quantity }}
-                </div>
-            </vdr-labeled-data>
-            <vdr-labeled-data
-                *ngIf="modifyOrderInput.surcharges?.length"
-                [label]="
-                    'order.modification-adding-surcharges'
-                        | translate : { count: modifyOrderInput.surcharges?.length }
-                "
-            >
-                <div *ngFor="let surcharge of modifyOrderInput.surcharges" class="mb-1">
-                    {{ surcharge.description }}: {{ surcharge.price | localeCurrency : order.currencyCode }}
-                </div>
-            </vdr-labeled-data>
-            <vdr-labeled-data
-                *ngIf="shippingAddressForm.dirty"
-                [label]="'order.modification-updating-shipping-address' | translate"
-            >
-                {{ getModifiedFields(shippingAddressForm) }}
-            </vdr-labeled-data>
-            <vdr-labeled-data
-                *ngIf="billingAddressForm.dirty"
-                [label]="'order.modification-updating-billing-address' | translate"
-            >
-                {{ getModifiedFields(billingAddressForm) }}
-            </vdr-labeled-data>
-            <vdr-labeled-data *ngIf="couponCodesControl.dirty" [label]="'order.set-coupon-codes' | translate">
-                <div *ngFor="let change of couponCodeChanges" class="mb-1">{{ change }}</div>
-            </vdr-labeled-data>
+            <vdr-order-modification-summary
+                [orderSnapshot]="orderSnapshot"
+                [modifyOrderInput]="modifyOrderInput"
+                [addedLines]="addedLines"
+                [shippingAddressForm]="shippingAddressForm"
+                [billingAddressForm]="billingAddressForm"
+                [couponCodesControl]="couponCodesControl"
+            ></vdr-order-modification-summary>
 
             <div *ngIf="!hasModifications()" class="no-modifications">
                 {{ 'order.no-modifications-made' | translate }}
             </div>
 
-            <div *ngIf="hasModifications()" class="summary-controls">
+            <div class="summary-controls">
                 <vdr-form-field [label]="'order.note' | translate">
-                    <textarea [(ngModel)]="note" name="note" required></textarea>
+                    <textarea
+                        [(ngModel)]="note"
+                        name="note"
+                        required
+                        [disabled]="!hasModifications()"
+                    ></textarea>
                 </vdr-form-field>
                 <label class="flex items-center">
-                    <input type="checkbox" [(ngModel)]="recalculateShipping" />
+                    <input
+                        type="checkbox"
+                        [(ngModel)]="recalculateShipping"
+                        [disabled]="!hasModifications()"
+                    />
                     <div class="ml-1">{{ 'order.modification-recalculate-shipping' | translate }}</div>
                 </label>
                 <button
                     class="btn btn-primary mt-2"
+                    [disabled]="!hasModifications()"
                     (click)="previewAndModify(order)"
                 >
                     {{ 'order.preview-changes' | translate }}
@@ -203,7 +173,7 @@
                             type="number"
                             class="draft-qty mr-1"
                             min="0"
-                            [value]="line.quantity"
+                            [value]="getInitialLineQuantity(line.id)"
                             (input)="updateLineQuantity(line, $event.target.value)"
                         />
                         <button
@@ -287,7 +257,7 @@
                     <vdr-form-field
                         [label]="
                             'catalog.price-includes-tax-at'
-                                | translate : { rate: surchargeForm.get('taxRate')?.value }
+                                | translate : { rate: surchargeForm.get('taxRate')?.value ?? 0 }
                         "
                         for="priceIncludesTax"
                         ><input

+ 34 - 67
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts

@@ -8,7 +8,6 @@ import {
     Validators,
 } from '@angular/forms';
 import {
-    AddItemInput,
     CustomFieldConfig,
     DataService,
     ErrorResult,
@@ -21,7 +20,6 @@ import {
     OrderAddressFragment,
     OrderDetailFragment,
     OrderDetailQueryDocument,
-    OrderLineInput,
     ProductSelectorSearchQuery,
     SortOrder,
     SurchargeInput,
@@ -31,7 +29,8 @@ import {
 import { assertNever, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 import { EMPTY, Observable, of } from 'rxjs';
-import { mapTo, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
+import { mapTo, shareReplay, switchMap, take, takeUntil } from 'rxjs/operators';
+import { AddedLine, ModifyOrderData, OrderSnapshot } from '../../common/modify-order-types';
 
 import { OrderTransitionService } from '../../providers/order-transition.service';
 import {
@@ -39,25 +38,7 @@ import {
     OrderEditsPreviewDialogComponent,
 } from '../order-edits-preview-dialog/order-edits-preview-dialog.component';
 
-type ProductSelectorItem = ProductSelectorSearchQuery['search']['items'][number];
-
-interface AddedLine {
-    id: string;
-    featuredAsset?: ProductSelectorItem['productAsset'] | null;
-    productVariant: {
-        id: string;
-        name: string;
-        sku: string;
-    };
-    unitPrice: number;
-    unitPriceWithTax: number;
-    quantity: number;
-}
-
-type ModifyOrderData = Omit<ModifyOrderInput, 'addItems' | 'adjustOrderLines'> & {
-    addItems: Array<AddItemInput & { customFields?: any }>;
-    adjustOrderLines: Array<OrderLineInput & { customFields?: any }>;
-};
+export type ProductSelectorItem = ProductSelectorSearchQuery['search']['items'][number];
 
 @Component({
     selector: 'vdr-order-editor',
@@ -78,6 +59,7 @@ export class OrderEditorComponent
     addItemCustomFieldsForm: UntypedFormGroup;
     addItemSelectedVariant: ProductSelectorItem | undefined;
     orderLineCustomFields: CustomFieldConfig[];
+    orderSnapshot: OrderSnapshot;
     modifyOrderInput: ModifyOrderData = {
         dryRun: true,
         orderId: '',
@@ -85,6 +67,7 @@ export class OrderEditorComponent
         adjustOrderLines: [],
         surcharges: [],
         note: '',
+        refunds: [],
         updateShippingAddress: {},
         updateBillingAddress: {},
     };
@@ -139,7 +122,8 @@ export class OrderEditorComponent
         this.addressCustomFields = this.getCustomFieldConfig('Address');
         this.modifyOrderInput.orderId = this.route.snapshot.paramMap.get('id') as string;
         this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
-        this.entity$.pipe(takeUntil(this.destroy$)).subscribe(order => {
+        this.entity$.pipe(take(1)).subscribe(order => {
+            this.orderSnapshot = this.createOrderSnapshot(order);
             if (order.couponCodes.length) {
                 this.couponCodesControl.setValue(order.couponCodes);
             }
@@ -227,43 +211,6 @@ export class OrderEditorComponent
             .filter(notNullOrUndefined);
     }
 
-    get adjustedLines(): string[] {
-        return (this.modifyOrderInput.adjustOrderLines || [])
-            .map(l => {
-                const line = this.entity?.lines.find(line => line.id === l.orderLineId);
-                if (line) {
-                    const delta = l.quantity - line.quantity;
-                    const sign = delta === 0 ? '' : delta > 0 ? '+' : '-';
-                    return delta
-                        ? `${sign}${Math.abs(delta)} ${line.productVariant.name}`
-                        : line.productVariant.name;
-                }
-            })
-            .filter(notNullOrUndefined);
-    }
-
-    get couponCodeChanges(): string[] {
-        const originalCodes = this.entity?.couponCodes || [];
-        const newCodes = this.couponCodesControl.value || [];
-        const addedCodes = newCodes.filter(c => !originalCodes.includes(c)).map(c => `+ ${c}`);
-        const removedCodes = originalCodes.filter(c => !newCodes.includes(c)).map(c => `- ${c}`);
-        return [...addedCodes, ...removedCodes];
-    }
-
-    getModifiedFields(formGroup: FormGroup): string {
-        if (!formGroup.dirty) {
-            return '';
-        }
-        return Object.entries(formGroup.controls)
-            .map(([key, control]) => {
-                if (control.dirty) {
-                    return key;
-                }
-            })
-            .filter(notNullOrUndefined)
-            .join(', ');
-    }
-
     private getIdForAddedItem(row: ModifyOrderData['addItems'][number]) {
         return `added-${row.productVariantId}-${JSON.stringify(row.customFields || {})}`;
     }
@@ -294,6 +241,16 @@ export class OrderEditorComponent
         );
     }
 
+    getInitialLineQuantity(lineId: string): number {
+        const adjustedLine = this.modifyOrderInput.adjustOrderLines?.find(l => l.orderLineId === lineId);
+        if (adjustedLine) {
+            return adjustedLine.quantity;
+        } else {
+            const line = this.orderSnapshot.lines.find(l => l.id === lineId);
+            return line ? line.quantity : 0;
+        }
+    }
+
     updateLineQuantity(line: OrderDetailFragment['lines'][number] | AddedLine, quantity: string) {
         const { adjustOrderLines } = this.modifyOrderInput;
         if (this.isAddedLine(line)) {
@@ -442,7 +399,6 @@ export class OrderEditorComponent
                 recalculateShipping: this.recalculateShipping,
             },
         };
-        const originalTotalWithTax = order.totalWithTax;
         this.dataService.order
             .modifyOrder(input)
             .pipe(
@@ -453,10 +409,14 @@ export class OrderEditorComponent
                                 size: 'xl',
                                 closable: false,
                                 locals: {
-                                    originalTotalWithTax,
                                     order: modifyOrder,
+                                    orderSnapshot: this.orderSnapshot,
                                     orderLineCustomFields: this.orderLineCustomFields,
                                     modifyOrderInput: input,
+                                    addedLines: this.addedLines,
+                                    shippingAddressForm: this.shippingAddressForm,
+                                    billingAddressForm: this.billingAddressForm,
+                                    couponCodesControl: this.couponCodesControl,
                                 },
                             });
                         case 'InsufficientStockError':
@@ -490,15 +450,13 @@ export class OrderEditorComponent
                             dryRun: false,
                         };
                         if (result.result === OrderEditResultType.Refund) {
-                            wetRunInput.refund = {
-                                paymentId: result.refundPaymentId,
-                                reason: result.refundNote,
-                            };
+                            wetRunInput.refunds = result.refunds;
                         }
                         return this.dataService.order.modifyOrder(wetRunInput).pipe(
                             switchMap(({ modifyOrder }) => {
                                 if (modifyOrder.__typename === 'Order') {
-                                    const priceDelta = modifyOrder.totalWithTax - originalTotalWithTax;
+                                    const priceDelta =
+                                        modifyOrder.totalWithTax - this.orderSnapshot.totalWithTax;
                                     const nextState =
                                         0 < priceDelta ? 'ArrangingAdditionalPayment' : this.previousState;
 
@@ -536,6 +494,15 @@ export class OrderEditorComponent
         }
     }
 
+    private createOrderSnapshot(order: OrderDetailFragment): OrderSnapshot {
+        return {
+            totalWithTax: order.totalWithTax,
+            currencyCode: order.currencyCode,
+            couponCodes: order.couponCodes,
+            lines: [...order.lines].map(line => ({ ...line })),
+        };
+    }
+
     protected setFormValues(entity: OrderDetailFragment, languageCode: LanguageCode): void {
         /* not used */
     }

+ 70 - 25
packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.html

@@ -1,29 +1,74 @@
 <ng-template vdrDialogTitle>{{ 'order.confirm-modifications' | translate }}</ng-template>
-<vdr-order-table [order]="order" [orderLineCustomFields]="orderLineCustomFields"></vdr-order-table>
-
-<h4 class="h4">
-    {{ 'order.modify-order-price-difference' | translate }}:
-    <strong>{{ priceDifference | localeCurrency: order.currencyCode }}</strong>
-</h4>
-<div *ngIf="priceDifference < 0">
-<clr-select-container>
-    <label>{{ 'order.payment-to-refund' | translate }}</label>
-    <select clrSelect name="options" [(ngModel)]="selectedPayment">
-        <option
-            *ngFor="let payment of order.payments"
-            [ngValue]="payment"
-        >
-            #{{ payment.id }} {{ payment.method }}:
-            {{ payment.amount | localeCurrency: order.currencyCode }}
-        </option>
-    </select>
-</clr-select-container>
-    <label class="clr-control-label">{{ 'order.refund-cancellation-reason' | translate }}</label>
-    <textarea [(ngModel)]="refundNote" name="refundNote" clrTextarea required></textarea>
+<vdr-order-table
+    [order]="order"
+    [orderLineCustomFields]="orderLineCustomFields"
+    class="order-edits-preview-table"
+></vdr-order-table>
+<div class="payments-wrapper mb-2">
+    <div class="flex-spacer">
+        <vdr-payment-for-refund-selector
+            *ngIf="priceDifference < 0"
+            class=""
+            [refundablePayments]="refundablePayments"
+            (paymentSelected)="onPaymentSelected($event.payment, $event.selected)"
+            [order]="order"
+        ></vdr-payment-for-refund-selector>
+    </div>
+    <div class="flex-spacer">
+        <vdr-card [title]="'order.modify-order' | translate">
+            <vdr-order-modification-summary
+                [orderSnapshot]="orderSnapshot"
+                [modifyOrderInput]="modifyOrderInput"
+                [addedLines]="addedLines"
+                [shippingAddressForm]="shippingAddressForm"
+                [billingAddressForm]="billingAddressForm"
+                [couponCodesControl]="couponCodesControl"
+            ></vdr-order-modification-summary>
+            <vdr-labeled-data [label]="'order.note' | translate">
+                {{ modifyOrderInput.note || '-' }}
+            </vdr-labeled-data>
+            <vdr-form-field
+                [label]="'order.refund-cancellation-reason' | translate"
+                *ngIf="priceDifference < 0"
+            >
+                <textarea [(ngModel)]="refundNote" name="refundNote" required></textarea>
+            </vdr-form-field>
+        </vdr-card>
+    </div>
 </div>
 <ng-template vdrDialogButtons>
-    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
-    <button type="submit" (click)="submit()" [disabled]="priceDifference < 0 && !selectedPayment" class="btn btn-primary">
-        {{ 'common.confirm' | translate }}
-    </button>
+    <div>
+        <div class="errors">
+            <clr-alert
+                class="mb-1"
+                *ngIf="priceDifference < 0 && !refundsCoverDifference()"
+                [clrAlertType]="'danger'"
+                [clrAlertClosable]="false"
+            >
+                <clr-alert-item>
+                    {{ 'order.refund-total-warning' | translate }}
+                </clr-alert-item>
+            </clr-alert>
+        </div>
+        <div class="modal-buttons">
+            <clr-alert class="" [clrAlertType]="'primary'" [clrAlertClosable]="false">
+                <clr-alert-item>
+                    {{ 'order.modify-order-price-difference' | translate }}
+                    {{ 0 < priceDifference ? '+' : ''
+                    }}{{ priceDifference | localeCurrency : order.currencyCode }}
+                </clr-alert-item>
+            </clr-alert>
+            <button type="button" class="btn" (click)="cancel()">
+                {{ 'common.cancel' | translate }}
+            </button>
+            <button
+                type="submit"
+                (click)="submit()"
+                [disabled]="priceDifference < 0 ? !refundsCoverDifference() : false"
+                class="btn btn-primary"
+            >
+                {{ 'common.confirm' | translate }}
+            </button>
+        </div>
+    </div>
 </ng-template>

+ 15 - 0
packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.scss

@@ -0,0 +1,15 @@
+::ng-deep .order-edits-preview-table .table-wrapper {
+  max-width: initial !important;
+}
+
+.payments-wrapper {
+  display: flex;
+  gap: calc(var(--space-unit) * 2);
+}
+
+.modal-buttons {
+  display: flex;
+  justify-content: flex-end;
+  gap: 0.6rem;
+  gap: var(--clr-modal-footer-gap, 0.6rem);
+}

+ 64 - 9
packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.ts

@@ -1,5 +1,15 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
-import { CustomFieldConfig, Dialog, ModifyOrderInput, OrderDetailFragment } from '@vendure/admin-ui/core';
+import { FormControl, Validators } from '@angular/forms';
+import {
+    AdministratorRefundInput,
+    CustomFieldConfig,
+    Dialog,
+    ModifyOrderInput,
+    OrderDetailFragment,
+} from '@vendure/admin-ui/core';
+import { getRefundablePayments, RefundablePayment } from '../../common/get-refundable-payments';
+import { AddedLine, OrderSnapshot } from '../../common/modify-order-types';
+import { OrderEditorComponent } from '../order-editor/order-editor.component';
 
 export enum OrderEditResultType {
     Refund,
@@ -10,8 +20,7 @@ export enum OrderEditResultType {
 
 interface OrderEditsRefundResult {
     result: OrderEditResultType.Refund;
-    refundPaymentId: string;
-    refundNote?: string;
+    refunds: AdministratorRefundInput[];
 }
 interface OrderEditsPaymentResult {
     result: OrderEditResultType.Payment;
@@ -36,21 +45,60 @@ type OrderEditResult =
 })
 export class OrderEditsPreviewDialogComponent implements OnInit, Dialog<OrderEditResult> {
     // Passed in via the modalService
-    order: OrderDetailFragment;
-    originalTotalWithTax: number;
     orderLineCustomFields: CustomFieldConfig[];
+    order: OrderDetailFragment;
+    orderSnapshot: OrderSnapshot;
     modifyOrderInput: ModifyOrderInput;
+    addedLines: AddedLine[];
+    shippingAddressForm: OrderEditorComponent['shippingAddressForm'];
+    billingAddressForm: OrderEditorComponent['billingAddressForm'];
+    couponCodesControl: FormControl<string[] | null>;
 
-    selectedPayment?: NonNullable<OrderDetailFragment['payments']>[number];
+    refundablePayments: RefundablePayment[];
     refundNote: string;
     resolveWith: (result?: OrderEditResult) => void;
 
     get priceDifference(): number {
-        return this.order.totalWithTax - this.originalTotalWithTax;
+        return this.order.totalWithTax - this.orderSnapshot.totalWithTax;
+    }
+
+    get amountToRefundTotal(): number {
+        return this.refundablePayments.reduce(
+            (total, payment) => total + payment.amountToRefundControl.value,
+            0,
+        );
     }
 
     ngOnInit() {
         this.refundNote = this.modifyOrderInput.note || '';
+        this.refundablePayments = getRefundablePayments(this.order.payments || []);
+        this.refundablePayments.forEach(rp => {
+            rp.amountToRefundControl.addValidators(Validators.max(this.priceDifference * -1));
+        });
+        if (this.priceDifference < 0 && this.refundablePayments.length) {
+            this.onPaymentSelected(this.refundablePayments[0], true);
+        }
+    }
+
+    onPaymentSelected(payment: RefundablePayment, selected: boolean) {
+        if (selected) {
+            const outstandingRefundAmount =
+                this.priceDifference * -1 -
+                this.refundablePayments
+                    .filter(p => p.id !== payment.id)
+                    .reduce((total, p) => total + p.amountToRefundControl.value, 0);
+            if (0 < outstandingRefundAmount) {
+                payment.amountToRefundControl.setValue(
+                    Math.min(outstandingRefundAmount, payment.refundableAmount),
+                );
+            }
+        } else {
+            payment.amountToRefundControl.setValue(0);
+        }
+    }
+
+    refundsCoverDifference(): boolean {
+        return this.priceDifference * -1 === this.amountToRefundTotal;
     }
 
     cancel() {
@@ -68,8 +116,15 @@ export class OrderEditsPreviewDialogComponent implements OnInit, Dialog<OrderEdi
             this.resolveWith({
                 result: OrderEditResultType.Refund,
                 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                refundPaymentId: this.selectedPayment!.id,
-                refundNote: this.refundNote,
+                refunds: this.refundablePayments
+                    .filter(rp => rp.selected && 0 < rp.amountToRefundControl.value)
+                    .map(payment => {
+                        return {
+                            reason: this.refundNote || this.modifyOrderInput.note,
+                            paymentId: payment.id,
+                            amount: payment.amountToRefundControl.value,
+                        };
+                    }),
             });
         } else {
             this.resolveWith({

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/payment-for-refund-selector/payment-for-refund-selector.component.css → packages/admin-ui/src/lib/order/src/components/order-modification-summary/order-modification-summary.component.css


+ 48 - 0
packages/admin-ui/src/lib/order/src/components/order-modification-summary/order-modification-summary.component.html

@@ -0,0 +1,48 @@
+<vdr-labeled-data
+        *ngIf="modifyOrderInput.adjustOrderLines?.length"
+        [label]="
+                    'order.modification-adjusting-lines'
+                        | translate : { count: modifyOrderInput.adjustOrderLines?.length }
+                "
+>
+    <div *ngFor="let line of adjustedLines" class="mb-1">
+        {{ line }}
+    </div>
+</vdr-labeled-data>
+<vdr-labeled-data
+        *ngIf="modifyOrderInput.addItems?.length"
+        [label]="
+                    'order.modification-adding-items'
+                        | translate : { count: modifyOrderInput.addItems?.length }
+                "
+>
+    <div *ngFor="let item of addedLines">
+        {{ item.productVariant.name }} x {{ item.quantity }}
+    </div>
+</vdr-labeled-data>
+<vdr-labeled-data
+        *ngIf="modifyOrderInput.surcharges?.length"
+        [label]="
+                    'order.modification-adding-surcharges'
+                        | translate : { count: modifyOrderInput.surcharges?.length }
+                "
+>
+    <div *ngFor="let surcharge of modifyOrderInput.surcharges" class="mb-1">
+        {{ surcharge.description }}: {{ surcharge.price | localeCurrency : orderSnapshot.currencyCode }}
+    </div>
+</vdr-labeled-data>
+<vdr-labeled-data
+        *ngIf="getModifiedFields(shippingAddressForm) as updatedShippingFields"
+        [label]="'order.modification-updating-shipping-address' | translate"
+>
+    {{ updatedShippingFields }}
+</vdr-labeled-data>
+<vdr-labeled-data
+        *ngIf="getModifiedFields(billingAddressForm) as updatedBillingFields"
+        [label]="'order.modification-updating-billing-address' | translate"
+>
+    {{ updatedBillingFields }}
+</vdr-labeled-data>
+<vdr-labeled-data *ngIf="couponCodeChanges.length" [label]="'order.set-coupon-codes' | translate">
+    <div *ngFor="let change of couponCodeChanges" class="mb-1">{{ change }}</div>
+</vdr-labeled-data>

+ 57 - 0
packages/admin-ui/src/lib/order/src/components/order-modification-summary/order-modification-summary.component.ts

@@ -0,0 +1,57 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { FormControl, FormGroup } from '@angular/forms';
+import { OrderEditorComponent } from '@vendure/admin-ui/order';
+import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { AddedLine, ModifyOrderData, OrderSnapshot } from '../../common/modify-order-types';
+
+@Component({
+    selector: 'vdr-order-modification-summary',
+    templateUrl: './order-modification-summary.component.html',
+    styleUrls: ['./order-modification-summary.component.css'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class OrderModificationSummaryComponent {
+    @Input() orderSnapshot: OrderSnapshot;
+    @Input() modifyOrderInput: ModifyOrderData;
+    @Input() addedLines: AddedLine[];
+    @Input() shippingAddressForm: OrderEditorComponent['shippingAddressForm'];
+    @Input() billingAddressForm: OrderEditorComponent['billingAddressForm'];
+    @Input() couponCodesControl: FormControl<string[] | null>;
+
+    get adjustedLines(): string[] {
+        return (this.modifyOrderInput.adjustOrderLines || [])
+            .map(l => {
+                const line = this.orderSnapshot.lines.find(line => line.id === l.orderLineId);
+                if (line) {
+                    const delta = l.quantity - line.quantity;
+                    const sign = delta === 0 ? '' : delta > 0 ? '+' : '-';
+                    return delta
+                        ? `${sign}${Math.abs(delta)} ${line.productVariant.name}`
+                        : line.productVariant.name;
+                }
+            })
+            .filter(notNullOrUndefined);
+    }
+
+    getModifiedFields(formGroup: FormGroup): string {
+        if (!formGroup.dirty) {
+            return '';
+        }
+        return Object.entries(formGroup.controls)
+            .map(([key, control]) => {
+                if (control.dirty) {
+                    return key;
+                }
+            })
+            .filter(notNullOrUndefined)
+            .join(', ');
+    }
+
+    get couponCodeChanges(): string[] {
+        const originalCodes = this.orderSnapshot.couponCodes || [];
+        const newCodes = this.couponCodesControl.value || [];
+        const addedCodes = newCodes.filter(c => !originalCodes.includes(c)).map(c => `+ ${c}`);
+        const removedCodes = originalCodes.filter(c => !newCodes.includes(c)).map(c => `- ${c}`);
+        return [...addedCodes, ...removedCodes];
+    }
+}

+ 3 - 0
packages/admin-ui/src/lib/order/src/components/payment-for-refund-selector/payment-for-refund-selector.component.scss

@@ -0,0 +1,3 @@
+:host {
+    display: block;
+}

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/payment-for-refund-selector/payment-for-refund-selector.component.ts

@@ -5,7 +5,7 @@ import { RefundablePayment } from '../../common/get-refundable-payments';
 @Component({
     selector: 'vdr-payment-for-refund-selector',
     templateUrl: './payment-for-refund-selector.component.html',
-    styleUrls: ['./payment-for-refund-selector.component.css'],
+    styleUrls: ['./payment-for-refund-selector.component.scss'],
     changeDetection: ChangeDetectionStrategy.Default,
 })
 export class PaymentForRefundSelectorComponent {

+ 2 - 0
packages/admin-ui/src/lib/order/src/order.module.ts

@@ -49,6 +49,7 @@ import { SimpleItemListComponent } from './components/simple-item-list/simple-it
 import { createRoutes } from './order.routes';
 import { OrderDataTableComponent } from './components/order-data-table/order-data-table.component';
 import { PaymentForRefundSelectorComponent } from './components/payment-for-refund-selector/payment-for-refund-selector.component';
+import { OrderModificationSummaryComponent } from './components/order-modification-summary/order-modification-summary.component';
 
 @NgModule({
     imports: [SharedModule, RouterModule.forChild([])],
@@ -100,6 +101,7 @@ import { PaymentForRefundSelectorComponent } from './components/payment-for-refu
         OrderDataTableComponent,
         OrderTotalColumnComponent,
         PaymentForRefundSelectorComponent,
+        OrderModificationSummaryComponent,
     ],
     exports: [OrderCustomFieldsCardComponent],
 })

+ 2 - 0
packages/admin-ui/src/lib/order/src/public_api.ts

@@ -45,3 +45,5 @@ export * from './order.module';
 export * from './order.routes';
 export * from './providers/order-transition.service';
 export * from './providers/routing/order.guard';
+export { AddedLine } from './common/modify-order-types';
+export { ModifyOrderData } from './common/modify-order-types';