Skip to content

Commit

Permalink
feat(core): 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 9a1530c commit cf91a9e
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 20 deletions.
6 changes: 6 additions & 0 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export type AdministratorPaymentInput = {
};

export type AdministratorRefundInput = {
amount: Scalars['Money']['input'];
paymentId: Scalars['ID']['input'];
reason?: InputMaybe<Scalars['String']['input']>;
};
Expand Down Expand Up @@ -2478,7 +2479,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
11 changes: 11 additions & 0 deletions packages/core/src/api/schema/admin-api/order.api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ input AdministratorPaymentInput {
input AdministratorRefundInput {
paymentId: ID!
reason: String
"""
The amount to be refunded to this particular Payment. This was introduced in
v2.2.0 as the preferred way to specify the refund amount. The `lines`, `shipping` and `adjustment`
fields will be removed in a future version.
"""
amount: Money
}

input ModifyOrderOptions {
Expand Down Expand Up @@ -183,7 +189,12 @@ input ModifyOrderInput {
updateShippingAddress: UpdateOrderAddressInput
updateBillingAddress: UpdateOrderAddressInput
note: String
"""
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: AdministratorRefundInput
refunds: [AdministratorRefundInput!]
options: ModifyOrderOptions
couponCodes: [String!]
}
Expand Down
62 changes: 43 additions & 19 deletions packages/core/src/service/helpers/order-modifier/order-modifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,13 +387,19 @@ export class OrderModifier {
const { orderItemsLimit } = this.configService.orderOptions;
let currentItemsCount = summate(order.lines, 'quantity');
const updatedOrderLineIds: ID[] = [];
const refundInput: RefundOrderInput = {
const refundInputArray = Array.isArray(input.refunds)
? input.refunds
: input.refund
? [input.refund]
: [];
const refundInputs: RefundOrderInput[] = refundInputArray.map(refund => ({
lines: [],
adjustment: 0,
shipping: 0,
paymentId: input.refund?.paymentId || '',
reason: input.refund?.reason || input.note,
};
paymentId: refund.paymentId,
amount: refund.amount,
reason: refund.reason || input.note,
}));

for (const row of input.addItems ?? []) {
const { productVariantId, quantity } = row;
Expand Down Expand Up @@ -477,9 +483,12 @@ export class OrderModifier {

if (correctedQuantity < initialLineQuantity) {
const qtyDelta = initialLineQuantity - correctedQuantity;
refundInput.lines?.push({
orderLineId: orderLine.id,
quantity: qtyDelta,

refundInputs.forEach(ri => {
ri.lines.push({
orderLineId: orderLine.id,
quantity: qtyDelta,
});
});
}
}
Expand Down Expand Up @@ -509,7 +518,7 @@ export class OrderModifier {
order.surcharges.push(surcharge);
modification.surcharges.push(surcharge);
if (surcharge.priceWithTax < 0) {
refundInput.adjustment += Math.abs(surcharge.priceWithTax);
refundInputs.forEach(ri => (ri.adjustment += Math.abs(surcharge.priceWithTax)));
}
}
if (input.surcharges?.length) {
Expand Down Expand Up @@ -607,22 +616,34 @@ export class OrderModifier {
const newTotalWithTax = order.totalWithTax;
const delta = newTotalWithTax - initialTotalWithTax;
if (delta < 0) {
if (!input.refund) {
if (refundInputs.length === 0) {
return new RefundPaymentIdMissingError();
}
// If there are multiple refunds, we select the largest one as the
// "primary" refund to associate with the OrderModification.
const primaryRefund = refundInputs.slice().sort((a, b) => (b.amount || 0) - (a.amount || 0))[0];

// TODO: the following code can be removed once we remove the deprecated
// support for "shipping" and "adjustment" input fields for refunds
const shippingDelta = order.shippingWithTax - initialShippingWithTax;
if (shippingDelta < 0) {
refundInput.shipping = shippingDelta * -1;
primaryRefund.shipping = shippingDelta * -1;
}
refundInput.adjustment += await this.calculateRefundAdjustment(ctx, delta, refundInput);
const existingPayments = await this.getOrderPayments(ctx, order.id);
const payment = existingPayments.find(p => idsAreEqual(p.id, input.refund?.paymentId));
if (payment) {
const refund = await this.paymentService.createRefund(ctx, refundInput, order, payment);
if (!isGraphQlErrorResult(refund)) {
modification.refund = refund;
} else {
throw new InternalServerError(refund.message);
primaryRefund.adjustment += await this.calculateRefundAdjustment(ctx, delta, primaryRefund);
// end

for (const refundInput of refundInputs) {
const existingPayments = await this.getOrderPayments(ctx, order.id);
const payment = existingPayments.find(p => idsAreEqual(p.id, refundInput.paymentId));
if (payment) {
const refund = await this.paymentService.createRefund(ctx, refundInput, order, payment);
if (!isGraphQlErrorResult(refund)) {
if (idsAreEqual(payment.id, primaryRefund.paymentId)) {
modification.refund = refund;
}
} else {
throw new InternalServerError(refund.message);
}
}
}
}
Expand Down Expand Up @@ -653,6 +674,9 @@ export class OrderModifier {
* Because a Refund's amount is calculated based on the orderItems changed, plus shipping change,
* we need to make sure the amount gets adjusted to match any changes caused by other factors,
* i.e. promotions that were previously active but are no longer.
*
* TODO: Deprecated - can be removed once we remove support for the "shipping" & "adjustment" input
* fields for refunds.
*/
private async calculateRefundAdjustment(
ctx: RequestContext,
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/service/services/payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,17 @@ export class PaymentService {
input: RefundOrderInput,
): Promise<{ orderLinesTotal: number; total: number }> {
if (input.amount) {
// This is the new way of getting the refund amount
// after v2.2.0. It allows full control over the refund.
return { orderLinesTotal: 0, total: input.amount };
}

// This is the pre-v2.2.0 way of getting the refund amount.
// It calculates the refund amount based on the order lines to be refunded
// plus shipping and adjustment amounts. It is complex and prevents full
// control over refund amounts, especially when multiple payment methods
// are involved.
// It is deprecated and will be removed in a future version.
let refundOrderLinesTotal = 0;
const orderLines = await this.connection
.getRepository(ctx, OrderLine)
Expand Down
2 changes: 1 addition & 1 deletion schema-admin.json

Large diffs are not rendered by default.

0 comments on commit cf91a9e

Please sign in to comment.