Browse Source

Merge branch 'master' into minor

Michael Bromley 4 years ago
parent
commit
3023a9575d

+ 1 - 1
.github/workflows/publish_and_install.yml

@@ -19,7 +19,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        node-version: [12.x, 14.x]
+        node-version: [12.x, 14.x, 16.x]
       fail-fast: false
     steps:
     - uses: actions/checkout@v2

+ 8 - 2
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts

@@ -18,6 +18,7 @@ import {
     OrderDetail,
     OrderDetailFragment,
     OrderLineFragment,
+    Refund,
     RefundOrder,
     ServerConfigService,
     SortOrder,
@@ -253,8 +254,13 @@ export class OrderDetailComponent
     outstandingPaymentAmount(order: OrderDetailFragment): number {
         const paymentIsValid = (p: OrderDetail.Payments): boolean =>
             p.state !== 'Cancelled' && p.state !== 'Declined' && p.state !== 'Error';
-        const validPayments = order.payments?.filter(paymentIsValid).map(p => pick(p, ['amount'])) ?? [];
-        const amountCovered = summate(validPayments, 'amount');
+
+        let amountCovered = 0;
+        for (const payment of order.payments?.filter(paymentIsValid) ?? []) {
+            const refunds = payment.refunds.filter(r => r.state !== 'Failed') ?? [];
+            const refundsTotal = summate(refunds as Array<Required<Refund>>, 'total');
+            amountCovered += payment.amount - refundsTotal;
+        }
         return order.totalWithTax - amountCovered;
     }
 

+ 1 - 0
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -2856,6 +2856,7 @@ export type TestOrderFragmentFragment = Pick<
             | 'unitPriceWithTax'
             | 'unitPriceChangeSinceAdded'
             | 'unitPriceWithTaxChangeSinceAdded'
+            | 'proratedUnitPriceWithTax'
         > & {
             productVariant: Pick<ProductVariant, 'id'>;
             discounts: Array<

+ 1 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -31,6 +31,7 @@ export const TEST_ORDER_FRAGMENT = gql`
             unitPriceWithTax
             unitPriceChangeSinceAdded
             unitPriceWithTaxChangeSinceAdded
+            proratedUnitPriceWithTax
             productVariant {
                 id
             }

+ 67 - 3
packages/core/e2e/order-modification.e2e-spec.ts

@@ -16,6 +16,7 @@ import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 import { manualFulfillmentHandler } from '../src/config/fulfillment/manual-fulfillment-handler';
 import { orderFixedDiscount } from '../src/config/promotion/actions/order-fixed-discount-action';
+import { defaultPromotionActions } from '../src/config/promotion/index';
 
 import {
     failsToSettlePaymentMethod,
@@ -104,9 +105,6 @@ describe('Order modification', () => {
                     testFailingPaymentMethod,
                 ],
             },
-            promotionOptions: {
-                promotionConditions: [orderFixedDiscount],
-            },
             shippingOptions: {
                 shippingCalculators: [defaultShippingCalculator, testCalculator],
             },
@@ -1443,6 +1441,72 @@ describe('Order modification', () => {
         });
     });
 
+    // https://github.com/vendure-ecommerce/vendure/issues/890
+    describe('refund handling when promotions are active on order', () => {
+        it('refunds correct amount when order-level promotion applied', async () => {
+            await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
+                input: {
+                    name: '$5 off',
+                    couponCode: '5OFF2',
+                    enabled: true,
+                    conditions: [],
+                    actions: [
+                        {
+                            code: orderFixedDiscount.code,
+                            arguments: [{ name: 'discount', value: '500' }],
+                        },
+                    ],
+                },
+            });
+
+            await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
+            await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_1',
+                quantity: 2,
+            } as any);
+            await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(APPLY_COUPON_CODE, {
+                couponCode: '5OFF2',
+            });
+            await proceedToArrangingPayment(shopClient);
+            const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
+            orderGuard.assertSuccess(order);
+
+            const originalTotalWithTax = order.totalWithTax;
+
+            const { transitionOrderToState } = await adminClient.query<
+                AdminTransition.Mutation,
+                AdminTransition.Variables
+            >(ADMIN_TRANSITION_TO_STATE, {
+                id: order.id,
+                state: 'Modifying',
+            });
+            orderGuard.assertSuccess(transitionOrderToState);
+
+            expect(transitionOrderToState.state).toBe('Modifying');
+
+            const { modifyOrder } = await adminClient.query<ModifyOrder.Mutation, ModifyOrder.Variables>(
+                MODIFY_ORDER,
+                {
+                    input: {
+                        dryRun: false,
+                        orderId: order.id,
+                        adjustOrderLines: [{ orderLineId: order.lines[0].id, quantity: 1 }],
+                        refund: {
+                            paymentId: order.payments![0].id,
+                            reason: 'requested',
+                        },
+                    },
+                },
+            );
+            orderGuard.assertSuccess(modifyOrder);
+
+            expect(modifyOrder.totalWithTax).toBe(
+                originalTotalWithTax - order.lines[0].proratedUnitPriceWithTax,
+            );
+            expect(modifyOrder.payments![0].refunds![0].total).toBe(order.lines[0].proratedUnitPriceWithTax);
+        });
+    });
+
     async function assertOrderIsUnchanged(order: OrderWithLinesFragment) {
         const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             id: order.id,

+ 8 - 6
packages/core/e2e/role.e2e-spec.ts

@@ -56,18 +56,20 @@ describe('Role resolver', () => {
         expect(result.roles.totalItems).toBe(2);
     });
 
-    it(
-        'createRole with invalid permission',
-        assertThrowsWithMessage(async () => {
+    it('createRole with invalid permission', async () => {
+        try {
             await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
                 input: {
                     code: 'test',
                     description: 'test role',
-                    permissions: ['bad permission' as any],
+                    permissions: ['ReadCatalogx' as any],
                 },
             });
-        }, 'Variable "$input" got invalid value "bad permission" at "input.permissions[0]"'),
-    );
+            fail('Should have thrown');
+        } catch (e) {
+            expect(e.response.errors[0]?.extensions.code).toBe('BAD_USER_INPUT');
+        }
+    });
 
     it('createRole with no permissions includes Authenticated', async () => {
         const { createRole } = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(

+ 7 - 5
packages/core/e2e/shop-order.e2e-spec.ts

@@ -251,9 +251,8 @@ describe('Shop orders', () => {
                     }
                 }
             `;
-            it(
-                'addItemToOrder with private customFields errors',
-                assertThrowsWithMessage(async () => {
+            it('addItemToOrder with private customFields errors', async () => {
+                try {
                     await shopClient.query<AddItemToOrder.Mutation>(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS, {
                         productVariantId: 'T_2',
                         quantity: 1,
@@ -261,8 +260,11 @@ describe('Shop orders', () => {
                             privateField: 'oh no!',
                         },
                     });
-                }, 'Variable "$customFields" got invalid value { privateField: "oh no!" }; Field "privateField" is not defined by type "OrderLineCustomFieldsInput".'),
-            );
+                    fail('Should have thrown');
+                } catch (e) {
+                    expect(e.response.errors[0].extensions.code).toBe('BAD_USER_INPUT');
+                }
+            });
 
             it('addItemToOrder with equal customFields adds quantity to the existing OrderLine', async () => {
                 const { addItemToOrder: add1 } = await shopClient.query<AddItemToOrder.Mutation>(

+ 11 - 10
packages/core/package.json

@@ -38,17 +38,17 @@
     "cli/**/*"
   ],
   "dependencies": {
-    "@graphql-tools/stitch": "^6.2.4",
-    "@nestjs/common": "7.6.13",
-    "@nestjs/core": "7.6.13",
-    "@nestjs/graphql": "7.9.7",
-    "@nestjs/platform-express": "7.6.13",
-    "@nestjs/terminus": "7.1.0",
-    "@nestjs/testing": "7.6.13",
+    "@graphql-tools/stitch": "^7.5.3",
+    "@nestjs/common": "7.6.17",
+    "@nestjs/core": "7.6.17",
+    "@nestjs/graphql": "7.10.6",
+    "@nestjs/platform-express": "7.6.17",
+    "@nestjs/terminus": "7.2.0",
+    "@nestjs/testing": "7.6.17",
     "@nestjs/typeorm": "7.1.5",
     "@types/fs-extra": "^9.0.1",
     "@vendure/common": "^1.0.0",
-    "apollo-server-express": "2.21.0",
+    "apollo-server-express": "2.24.1",
     "bcrypt": "^5.0.0",
     "body-parser": "^1.19.0",
     "chalk": "^4.1.0",
@@ -59,9 +59,9 @@
     "fs-extra": "^9.0.1",
     "graphql": "15.5.0",
     "graphql-iso-date": "^3.6.1",
-    "graphql-tag": "^2.11.0",
+    "graphql-tag": "^2.12.4",
     "graphql-type-json": "^0.3.2",
-    "graphql-upload": "^11.0.0",
+    "graphql-upload": "^12.0.0",
     "http-proxy-middleware": "^1.0.5",
     "i18next": "^19.8.1",
     "i18next-express-middleware": "^2.0.0",
@@ -84,6 +84,7 @@
     "@types/faker": "^4.1.7",
     "@types/graphql-iso-date": "^3.4.0",
     "@types/graphql-type-json": "^0.3.2",
+    "@types/graphql-upload": "^8.0.4",
     "@types/gulp": "^4.0.7",
     "@types/mime-types": "^2.1.0",
     "@types/ms": "^0.7.31",

+ 10 - 0
packages/core/src/api/common/parse-context.ts

@@ -16,6 +16,16 @@ export type GraphQLContext = {
  * GraphQL & REST requests.
  */
 export function parseContext(context: ExecutionContext | ArgumentsHost): RestContext | GraphQLContext {
+    // TODO: Remove this check once this issue is resolved: https://github.com/nestjs/graphql/pull/1469
+    if ((context as ExecutionContext).getHandler?.()?.name === '__resolveType') {
+        return {
+            req: context.getArgs()[1].req,
+            res: context.getArgs()[1].res,
+            isGraphQL: false,
+            info: undefined,
+        };
+    }
+
     const graphQlContext = GqlExecutionContext.create(context as ExecutionContext);
     const info = graphQlContext.getInfo();
     let req: Request;

+ 6 - 2
packages/core/src/api/config/generate-auth-types.ts

@@ -1,4 +1,4 @@
-import { stitchSchemas } from '@graphql-tools/stitch';
+import { stitchSchemas, ValidationLevel } from '@graphql-tools/stitch';
 import {
     buildASTSchema,
     GraphQLInputFieldConfigMap,
@@ -46,5 +46,9 @@ export function generateAuthenticationTypes(
         fields,
     });
 
-    return stitchSchemas({ schemas: [schema, ...strategySchemas, [authenticationInput]] });
+    return stitchSchemas({
+        subschemas: [schema, ...strategySchemas],
+        types: [authenticationInput],
+        typeMergingOptions: { validationSettings: { validationLevel: ValidationLevel.Off } },
+    });
 }

+ 1 - 1
packages/core/src/api/config/generate-list-options.spec.ts

@@ -199,11 +199,11 @@ describe('generateListOptions()', () => {
         expect(printType(result.getType('PersonListOptions')!)).toBe(
             removeLeadingWhitespace(`
                     input PersonListOptions {
+                      categoryId: ID
                       skip: Int
                       take: Int
                       sort: PersonSortParameter
                       filter: PersonFilterParameter
-                      categoryId: ID
                     }`),
         );
 

+ 6 - 2
packages/core/src/api/config/generate-list-options.ts

@@ -1,4 +1,4 @@
-import { stitchSchemas } from '@graphql-tools/stitch';
+import { stitchSchemas, ValidationLevel } from '@graphql-tools/stitch';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import {
     buildSchema,
@@ -74,7 +74,11 @@ export function generateListOptions(typeDefsOrSchema: string | GraphQLSchema): G
             generatedTypes.push(generatedListOptions);
         }
     }
-    return stitchSchemas({ schemas: [schema, generatedTypes] });
+    return stitchSchemas({
+        subschemas: [schema],
+        types: generatedTypes,
+        typeMergingOptions: { validationSettings: { validationLevel: ValidationLevel.Off } },
+    });
 }
 
 function isListQueryType(type: GraphQLOutputType): type is GraphQLObjectType {

+ 7 - 3
packages/core/src/api/config/generate-permissions.ts

@@ -1,5 +1,5 @@
-import { stitchSchemas } from '@graphql-tools/stitch';
-import { GraphQLEnumType, GraphQLInputObjectType, GraphQLSchema } from 'graphql';
+import { stitchSchemas, ValidationLevel } from '@graphql-tools/stitch';
+import { GraphQLEnumType, GraphQLSchema } from 'graphql';
 import { GraphQLEnumValueConfigMap } from 'graphql/type/definition';
 
 import { getAllPermissionsMetadata } from '../../common/constants';
@@ -35,5 +35,9 @@ export function generatePermissionEnum(
         values,
     });
 
-    return stitchSchemas({ schemas: [schema, [permissionsEnum]] });
+    return stitchSchemas({
+        subschemas: [schema],
+        types: [permissionsEnum],
+        typeMergingOptions: { validationSettings: { validationLevel: ValidationLevel.Off } },
+    });
 }

+ 1 - 1
packages/core/src/entity/order-line/order-line.entity.ts

@@ -209,7 +209,7 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
      * is specified, then all adjustments are removed.
      */
     clearAdjustments(type?: AdjustmentType) {
-        this.activeItems.forEach(item => item.clearAdjustments(type));
+        this.items.forEach(item => item.clearAdjustments(type));
     }
 
     private firstActiveItemPropOr<K extends keyof OrderItem>(

+ 1 - 1
packages/core/src/migrate.ts

@@ -174,7 +174,7 @@ function createConnectionOptions(userConfig: Partial<VendureConfig>): Connection
  * See https://github.com/typeorm/typeorm/issues/2576#issuecomment-499506647
  */
 async function disableForeignKeysForSqLite<T>(connection: Connection, work: () => Promise<T>): Promise<T> {
-    const isSqLite = connection.options.type === 'sqlite';
+    const isSqLite = connection.options.type === 'sqlite' || connection.options.type === 'better-sqlite3';
     if (isSqLite) {
         await connection.query('PRAGMA foreign_keys=OFF');
     }

+ 17 - 21
packages/core/src/service/services/payment.service.ts

@@ -133,33 +133,28 @@ export class PaymentService {
             payment,
             paymentMethod.handler.args,
         );
+        const fromState = payment.state;
+        let toState: PaymentState;
+        payment.metadata = this.mergePaymentMetadata(payment.metadata, settlePaymentResult.metadata);
         if (settlePaymentResult.success) {
-            const fromState = payment.state;
-            const toState = 'Settled';
-            try {
-                await this.paymentStateMachine.transition(ctx, payment.order, payment, toState);
-            } catch (e) {
-                const transitionError = ctx.translate(e.message, { fromState, toState });
-                return new PaymentStateTransitionError(transitionError, fromState, toState);
-            }
-            payment.metadata = this.mergePaymentMetadata(payment.metadata, settlePaymentResult.metadata);
-            await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
-            this.eventBus.publish(
-                new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order),
-            );
+            toState = 'Settled';
         } else {
+            toState = settlePaymentResult.state || 'Error';
             payment.errorMessage = settlePaymentResult.errorMessage;
-            payment.metadata = this.mergePaymentMetadata(payment.metadata, settlePaymentResult.metadata);
-            await this.paymentStateMachine.transition(
-                ctx,
-                payment.order,
-                payment,
-                settlePaymentResult.state || 'Error',
-            );
-            await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
         }
+        try {
+            await this.paymentStateMachine.transition(ctx, payment.order, payment, toState);
+        } catch (e) {
+            const transitionError = ctx.translate(e.message, { fromState, toState });
+            return new PaymentStateTransitionError(transitionError, fromState, toState);
+        }
+        await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
+        this.eventBus.publish(
+            new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order),
+        );
         return payment;
     }
+
     /**
      * Creates a Payment from the manual payment mutation in the Admin API
      */
@@ -202,6 +197,7 @@ export class PaymentService {
             const nonFailedRefunds = payment.refunds?.filter(refund => refund.state !== 'Failed') ?? [];
             return summate(nonFailedRefunds, 'total');
         }
+
         const existingNonFailedRefunds =
             orderWithRefunds.payments
                 ?.reduce((refunds, p) => [...refunds, ...p.refunds], [] as Refund[])

+ 2 - 2
packages/email-plugin/src/dev-mailbox.ts

@@ -1,6 +1,6 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { Channel, RequestContext } from '@vendure/core';
-import express from 'express';
+import express, { Router } from 'express';
 import fs from 'fs-extra';
 import http from 'http';
 import path from 'path';
@@ -17,7 +17,7 @@ export class DevMailbox {
         event: EventWithContext,
     ) => void | undefined;
 
-    serve(options: EmailPluginDevModeOptions) {
+    serve(options: EmailPluginDevModeOptions): Router {
         const { outputPath, handlers } = options;
         const server = express.Router();
         server.get('/', (req, res) => {

File diff suppressed because it is too large
+ 349 - 351
yarn.lock


Some files were not shown because too many files changed in this diff