Skip to content

Commit

Permalink
feat(admin-ui): Enable multiple refunds on an order modification
Browse files Browse the repository at this point in the history
Relates to #2393
  • Loading branch information
michaelbromley committed Jan 22, 2024
1 parent cf91a9e commit 9b3aa65
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 156 deletions.
6 changes: 6 additions & 0 deletions packages/admin-ui/src/lib/core/src/common/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export type AdministratorPaymentInput = {
};

export type AdministratorRefundInput = {
amount: Scalars['Money']['input'];
paymentId: Scalars['ID']['input'];
reason?: InputMaybe<Scalars['String']['input']>;
};
Expand Down Expand Up @@ -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>;
Expand Down
33 changes: 33 additions & 0 deletions packages/admin-ui/src/lib/order/src/common/modify-order-types.ts
Original file line number Diff line number Diff line change
@@ -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 }>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Validators,
} from '@angular/forms';
import {
AddItemInput,
CustomFieldConfig,
DataService,
ErrorResult,
Expand All @@ -21,7 +20,6 @@ import {
OrderAddressFragment,
OrderDetailFragment,
OrderDetailQueryDocument,
OrderLineInput,
ProductSelectorSearchQuery,
SortOrder,
SurchargeInput,
Expand All @@ -31,33 +29,16 @@ 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 {
OrderEditResultType,
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',
Expand All @@ -78,13 +59,15 @@ export class OrderEditorComponent
addItemCustomFieldsForm: UntypedFormGroup;
addItemSelectedVariant: ProductSelectorItem | undefined;
orderLineCustomFields: CustomFieldConfig[];
orderSnapshot: OrderSnapshot;
modifyOrderInput: ModifyOrderData = {
dryRun: true,
orderId: '',
addItems: [],
adjustOrderLines: [],
surcharges: [],
note: '',
refunds: [],
updateShippingAddress: {},
updateBillingAddress: {},
};
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 || {})}`;
}
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -442,7 +399,6 @@ export class OrderEditorComponent
recalculateShipping: this.recalculateShipping,
},
};
const originalTotalWithTax = order.totalWithTax;
this.dataService.order
.modifyOrder(input)
.pipe(
Expand All @@ -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':
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 */
}
Expand Down
Loading

0 comments on commit 9b3aa65

Please sign in to comment.