Explorar o código

feat(core): Enable multiple refunds on an order modification

Relates to #2393
Michael Bromley %!s(int64=2) %!d(string=hai) anos
pai
achega
cf91a9e1e9

+ 6 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -129,6 +129,7 @@ export type AdministratorPaymentInput = {
 };
 
 export type AdministratorRefundInput = {
+  amount: Scalars['Money']['input'];
   paymentId: Scalars['ID']['input'];
   reason?: InputMaybe<Scalars['String']['input']>;
 };
@@ -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>;

+ 11 - 0
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -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 {
@@ -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!]
 }

+ 43 - 19
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -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;
@@ -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,
+                        });
                     });
                 }
             }
@@ -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) {
@@ -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);
+                    }
                 }
             }
         }
@@ -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,

+ 9 - 0
packages/core/src/service/services/payment.service.ts

@@ -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)

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
schema-admin.json


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio