Browse Source

Merge branch 'master' into new-docs

Michael Bromley 2 years ago
parent
commit
4822de6df3

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.ts

@@ -207,7 +207,6 @@ export class ProductVariantDetailComponent
             .pipe(
                 take(1),
                 mergeMap(([variant, languageCode]) => {
-                    const formValue = this.detailForm.value;
                     const input = pick(
                         this.getUpdatedVariant(
                             variant,
@@ -400,6 +399,7 @@ export class ProductVariantDetailComponent
             assetIds: this.assetChanges.assets?.map(a => a.id),
             featuredAssetId: this.assetChanges.featuredAsset?.id,
             facetValueIds: variantFormGroup.value.facetValueIds,
+            taxCategoryId: variantFormGroup.value.taxCategoryId,
         } as UpdateProductVariantInput | CreateProductVariantInput;
     }
 }

+ 3 - 1
packages/admin-ui/src/lib/core/src/shared/components/affixed-input/affixed-input.component.scss

@@ -23,7 +23,8 @@
 ::ng-deep .has-prefix > {
     input[type="text"], input[type="number"] {
         border-top-left-radius: 0 !important;
-        border-bottom-left-radius: 0 !important
+        border-bottom-left-radius: 0 !important;
+        width: 100%;
     }
 }
 
@@ -37,6 +38,7 @@
     input[type="text"], input[type="number"] {
         border-top-right-radius: 0 !important;
         border-bottom-right-radius: 0 !important;
+        width: 100%;
     }
 }
 

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/relation-form-input/asset/relation-asset-input.component.scss

@@ -1,6 +1,8 @@
 .preview {
     cursor: pointer;
     border-radius: var(--border-radius);
+    max-width: 100px;
+    max-height: 100px;
 }
 
 .detail {

+ 6 - 2
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.ts

@@ -69,7 +69,8 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
     getUnfulfilledCount(line: OrderDetailFragment['lines'][number]): number {
         const fulfilled =
             this.order.fulfillments
-                ?.map(f => f.lines)
+                ?.filter(f => f.state !== 'Cancelled')
+                .map(f => f.lines)
                 .flat()
                 .filter(row => row.orderLineId === line.id)
                 .reduce((sum, row) => sum + row.quantity, 0) ?? 0;
@@ -81,12 +82,15 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
             (total, { fulfillCount }) => total + fulfillCount,
             0,
         );
+        const fulfillmentQuantityIsValid = Object.values(this.fulfillmentQuantities).every(
+            ({ fulfillCount, max }) => fulfillCount <= max,
+        );
         const formIsValid =
             configurableOperationValueIsValid(
                 this.fulfillmentHandlerDef,
                 this.fulfillmentHandlerControl.value,
             ) && this.fulfillmentHandlerControl.valid;
-        return formIsValid && 0 < totalCount;
+        return formIsValid && 0 < totalCount && fulfillmentQuantityIsValid;
     }
 
     select() {

+ 3 - 4
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts

@@ -257,10 +257,9 @@ export class OrderDetailComponent
     }
 
     canAddFulfillment(order: OrderDetailFragment): boolean {
-        const allFulfillmentLines: FulfillmentFragment['lines'] = (order.fulfillments ?? []).reduce(
-            (all, fulfillment) => [...all, ...fulfillment.lines],
-            [] as FulfillmentFragment['lines'],
-        );
+        const allFulfillmentLines: FulfillmentFragment['lines'] = (order.fulfillments ?? [])
+            .filter(fulfillment => fulfillment.state !== 'Cancelled')
+            .reduce((all, fulfillment) => [...all, ...fulfillment.lines], [] as FulfillmentFragment['lines']);
         let allItemsFulfilled = true;
         for (const line of order.lines) {
             const totalFulfilledCount = allFulfillmentLines

+ 65 - 1
packages/core/e2e/entity-hydrator.e2e-spec.ts

@@ -1,5 +1,14 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
-import { mergeConfig, Order, Product, ProductVariant } from '@vendure/core';
+import {
+    ChannelService,
+    EntityHydrator,
+    mergeConfig,
+    Order,
+    Product,
+    ProductVariant,
+    RequestContext,
+    ActiveOrderService,
+} from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
@@ -11,6 +20,7 @@ import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-conf
 import { HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin';
 import { UpdateChannelMutation, UpdateChannelMutationVariables } from './graphql/generated-e2e-admin-types';
 import {
+    AddItemToOrderDocument,
     AddItemToOrderMutation,
     AddItemToOrderMutationVariables,
     UpdatedOrderFragment,
@@ -225,6 +235,60 @@ describe('Entity hydration', () => {
         expect(hydrateChannel.customFields.thumb).toBeDefined();
         expect(hydrateChannel.customFields.thumb.id).toBe('T_2');
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/2013
+    describe('hydration of OrderLine ProductVariantPrices', () => {
+        let order: Order | undefined;
+
+        it('Create order with 3 items', async () => {
+            await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
+            await shopClient.query(AddItemToOrderDocument, {
+                productVariantId: '1',
+                quantity: 1,
+            });
+            await shopClient.query(AddItemToOrderDocument, {
+                productVariantId: '2',
+                quantity: 1,
+            });
+            const { addItemToOrder } = await shopClient.query(AddItemToOrderDocument, {
+                productVariantId: '3',
+                quantity: 1,
+            });
+            orderResultGuard.assertSuccess(addItemToOrder);
+            const channel = await server.app.get(ChannelService).getDefaultChannel();
+            // This is ugly, but in our real life example we use a CTX constructed by Vendure
+            const ctx = new RequestContext({
+                channel,
+                authorizedAsOwnerOnly: true,
+                apiType: 'shop',
+                isAuthorized: true,
+                session: {
+                    activeOrderId: addItemToOrder.id,
+                    activeChannelId: 1,
+                    user: {
+                        id: 2,
+                    },
+                } as any,
+            });
+            order = await server.app.get(ActiveOrderService).getActiveOrder(ctx, undefined);
+            await server.app.get(EntityHydrator).hydrate(ctx, order!, {
+                relations: ['lines.productVariant'],
+                applyProductVariantPrices: true,
+            });
+        });
+
+        it('Variant of orderLine 1 has a price', async () => {
+            expect(order!.lines[0].productVariant.priceWithTax).toBeGreaterThan(0);
+        });
+
+        it('Variant of orderLine 2 has a price', async () => {
+            expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0);
+        });
+
+        it('Variant of orderLine 3 has a price', async () => {
+            expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0);
+        });
+    });
 });
 
 function getVariantWithName(product: Product, name: string) {

+ 47 - 0
packages/core/e2e/order.e2e-spec.ts

@@ -35,6 +35,7 @@ import * as Codegen from './graphql/generated-e2e-admin-types';
 import {
     AddManualPaymentDocument,
     CanceledOrderFragment,
+    CreateFulfillmentDocument,
     ErrorCode,
     FulfillmentFragment,
     GetOrderDocument,
@@ -47,6 +48,7 @@ import {
     RefundFragment,
     SortOrder,
     StockMovementType,
+    TransitFulfillmentDocument,
 } from './graphql/generated-e2e-admin-types';
 import * as CodegenShop from './graphql/generated-e2e-shop-types';
 import {
@@ -2609,6 +2611,51 @@ describe('Orders resolver', () => {
 
             expect(order!.state).toBe('PaymentSettled');
         });
+
+        // https://github.com/vendure-ecommerce/vendure/issues/2191
+        it('correctly transitions order & fulfillment on partial fulfillment being shipped', async () => {
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
+            const { addItemToOrder } = await shopClient.query<
+                CodegenShop.AddItemToOrderMutation,
+                CodegenShop.AddItemToOrderMutationVariables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_6',
+                quantity: 3,
+            });
+            await proceedToArrangingPayment(shopClient);
+            orderGuard.assertSuccess(addItemToOrder);
+
+            const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
+            orderGuard.assertSuccess(order);
+
+            const { addFulfillmentToOrder } = await adminClient.query(CreateFulfillmentDocument, {
+                input: {
+                    lines: [{ orderLineId: order.lines[0].id, quantity: 2 }],
+                    handler: {
+                        code: manualFulfillmentHandler.code,
+                        arguments: [
+                            { name: 'method', value: 'Test2' },
+                            { name: 'trackingCode', value: '222' },
+                        ],
+                    },
+                },
+            });
+            fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
+
+            const { transitionFulfillmentToState } = await adminClient.query(TransitFulfillmentDocument, {
+                id: addFulfillmentToOrder.id,
+                state: 'Shipped',
+            });
+            fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
+
+            expect(transitionFulfillmentToState.id).toBe(addFulfillmentToOrder.id);
+            expect(transitionFulfillmentToState.state).toBe('Shipped');
+
+            const { order: order2 } = await adminClient.query(GetOrderDocument, {
+                id: order.id,
+            });
+            expect(order2?.state).toBe('PartiallyShipped');
+        });
     });
 });
 

+ 0 - 32
packages/core/src/config/entity-metadata/add-foreign-key-indices.ts

@@ -1,32 +0,0 @@
-import { Index } from 'typeorm';
-import { MetadataArgsStorage } from 'typeorm/metadata-args/MetadataArgsStorage';
-
-import { StockMovement } from '../../entity/stock-movement/stock-movement.entity';
-
-import { EntityMetadataModifier } from './entity-metadata-modifier';
-
-/**
- * @description
- * Dynamically adds `@Index()` metadata to all many-to-one relations. These are already added
- * by default in MySQL/MariaDB, but not in Postgres. So this modification can lead to improved
- * performance with Postgres - especially when dealing with large numbers of products, orders etc.
- *
- * See https://github.com/vendure-ecommerce/vendure/issues/1502
- *
- * TODO: In v2 we will add the Index to all relations manually, this making this redundant.
- */
-export const addForeignKeyIndices: EntityMetadataModifier = (metadata: MetadataArgsStorage) => {
-    for (const relationMetadata of metadata.relations) {
-        const { relationType, target } = relationMetadata;
-        if (relationType === 'many-to-one') {
-            const embeddedIn = metadata.embeddeds.find(e => e.type() === relationMetadata.target)?.target;
-            const targetClass = (embeddedIn ?? target) as FunctionConstructor;
-            if (typeof targetClass === 'function') {
-                const instance = new targetClass();
-                if (!(instance instanceof StockMovement)) {
-                    Index()(instance, relationMetadata.propertyName);
-                }
-            }
-        }
-    }
-};

+ 0 - 1
packages/core/src/config/index.ts

@@ -29,7 +29,6 @@ export * from './entity/bigint-money-strategy';
 export * from './entity/entity-id-strategy';
 export * from './entity/money-strategy';
 export * from './entity/uuid-id-strategy';
-export * from './entity-metadata/add-foreign-key-indices';
 export * from './entity-metadata/entity-metadata-modifier';
 export * from './fulfillment/default-fulfillment-process';
 export * from './fulfillment/fulfillment-handler';

+ 30 - 5
packages/core/src/service/helpers/utils/order-utils.ts

@@ -44,7 +44,10 @@ export function totalCoveredByPayments(order: Order, state?: PaymentState | Paym
  * Returns true if all (non-cancelled) OrderItems are delivered.
  */
 export function orderItemsAreDelivered(order: Order) {
-    return getOrderLinesFulfillmentStates(order).every(state => state === 'Delivered');
+    return (
+        getOrderLinesFulfillmentStates(order).every(state => state === 'Delivered') &&
+        !isOrderPartiallyFulfilled(order)
+    );
 }
 
 /**
@@ -52,7 +55,10 @@ export function orderItemsAreDelivered(order: Order) {
  */
 export function orderItemsArePartiallyDelivered(order: Order) {
     const states = getOrderLinesFulfillmentStates(order);
-    return states.some(state => state === 'Delivered') && !states.every(state => state === 'Delivered');
+    return (
+        states.some(state => state === 'Delivered') &&
+        (!states.every(state => state === 'Delivered') || isOrderPartiallyFulfilled(order))
+    );
 }
 
 function getOrderLinesFulfillmentStates(order: Order): Array<FulfillmentState | undefined> {
@@ -65,7 +71,7 @@ function getOrderLinesFulfillmentStates(order: Order): Array<FulfillmentState |
                     idsAreEqual(fl.orderLineId, line.id),
                 );
                 const totalFulfilled = summate(matchingFulfillmentLines, 'quantity');
-                if (totalFulfilled === line.quantity) {
+                if (0 < totalFulfilled) {
                     return matchingFulfillmentLines.map(l => l.fulfillment.state);
                 } else {
                     return undefined;
@@ -81,14 +87,20 @@ function getOrderLinesFulfillmentStates(order: Order): Array<FulfillmentState |
  */
 export function orderItemsArePartiallyShipped(order: Order) {
     const states = getOrderLinesFulfillmentStates(order);
-    return states.some(state => state === 'Shipped') && !states.every(state => state === 'Shipped');
+    return (
+        states.some(state => state === 'Shipped') &&
+        (!states.every(state => state === 'Shipped') || isOrderPartiallyFulfilled(order))
+    );
 }
 
 /**
  * Returns true if all (non-cancelled) OrderItems are shipped.
  */
 export function orderItemsAreShipped(order: Order) {
-    return getOrderLinesFulfillmentStates(order).every(state => state === 'Shipped');
+    return (
+        getOrderLinesFulfillmentStates(order).every(state => state === 'Shipped') &&
+        !isOrderPartiallyFulfilled(order)
+    );
 }
 
 /**
@@ -107,6 +119,19 @@ function getOrderFulfillmentLines(order: Order): FulfillmentLine[] {
         );
 }
 
+/**
+ * Returns true if Fulfillments exist for only some but not all of the
+ * order items.
+ */
+function isOrderPartiallyFulfilled(order: Order) {
+    const fulfillmentLines = getOrderFulfillmentLines(order);
+    const lines = fulfillmentLines.reduce((acc, item) => {
+        acc[item.orderLineId] = (acc[item.orderLineId] || 0) + item.quantity;
+        return acc;
+    }, {} as { [orderLineId: string]: number });
+    return order.lines.some(line => line.quantity > lines[line.id]);
+}
+
 export async function getOrdersFromLines(
     ctx: RequestContext,
     connection: TransactionalConnection,

+ 19 - 7
packages/core/src/service/services/order.service.ts

@@ -1233,7 +1233,7 @@ export class OrderService {
         ctx: RequestContext,
         input: FulfillOrderInput,
     ) {
-        const linesToBeFulfilled = await this.connection
+        const existingFulfillmentLines = await this.connection
             .getRepository(ctx, FulfillmentLine)
             .createQueryBuilder('fulfillmentLine')
             .leftJoinAndSelect('fulfillmentLine.orderLine', 'orderLine')
@@ -1244,13 +1244,25 @@ export class OrderService {
             .andWhere('fulfillment.state != :state', { state: 'Cancelled' })
             .getMany();
 
-        for (const lineToBeFulfilled of linesToBeFulfilled) {
-            const unfulfilledQuantity = lineToBeFulfilled.orderLine.quantity - lineToBeFulfilled.quantity;
-            const lineInput = input.lines.find(l =>
-                idsAreEqual(l.orderLineId, lineToBeFulfilled.orderLine.id),
+        for (const inputLine of input.lines) {
+            const existingFulfillmentLine = existingFulfillmentLines.find(l =>
+                idsAreEqual(l.orderLineId, inputLine.orderLineId),
             );
-            if (unfulfilledQuantity < (lineInput?.quantity ?? 0)) {
-                return true;
+            if (existingFulfillmentLine) {
+                const unfulfilledQuantity =
+                    existingFulfillmentLine.orderLine.quantity - existingFulfillmentLine.quantity;
+                if (unfulfilledQuantity < inputLine.quantity) {
+                    return true;
+                }
+            } else {
+                const orderLine = await this.connection.getEntityOrThrow(
+                    ctx,
+                    OrderLine,
+                    inputLine.orderLineId,
+                );
+                if (orderLine.quantity < inputLine.quantity) {
+                    return true;
+                }
             }
         }
         return false;

+ 0 - 4
packages/dev-server/load-testing/load-test-config.ts

@@ -1,7 +1,6 @@
 /* eslint-disable no-console */
 import { AssetServerPlugin } from '@vendure/asset-server-plugin';
 import {
-    addForeignKeyIndices,
     defaultConfig,
     DefaultJobQueuePlugin,
     DefaultLogger,
@@ -59,9 +58,6 @@ export function getLoadTestConfig(
             ...connectionOptions,
             synchronize: true,
         },
-        entityOptions: {
-            metadataModifiers: [addForeignKeyIndices],
-        },
         authOptions: {
             tokenMethod,
             requireVerification: false,