Răsfoiți Sursa

feat(core): Extend API with additional Fulfillment info

Relates to #1727. This commit adds

- `fulfillments` field to the `OrderLine` type
- `summary` field to `Fulfillment` type

These API extensions allow much more efficient querying of data required to display
fulfillment info in the Admin UI. It allows us to fetch much less data at the OrderItem
level, enabling much smaller and faster queries on the OrderDetail page.
Michael Bromley 3 ani în urmă
părinte
comite
3f0115b1d1

+ 7 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1571,12 +1571,18 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 /** Returned when there is an error in transitioning the Fulfillment state */
 export type FulfillmentStateTransitionError = ErrorResult & {
     errorCode: ErrorCode;
@@ -3167,6 +3173,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 

+ 8 - 0
packages/common/src/generated-shop-types.ts

@@ -1038,12 +1038,19 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    __typename?: 'FulfillmentLineSummary';
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 export enum GlobalFlag {
     TRUE = 'TRUE',
     FALSE = 'FALSE',
@@ -2025,6 +2032,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 

+ 8 - 0
packages/common/src/generated-types.ts

@@ -1605,12 +1605,19 @@ export type Fulfillment = Node & {
   createdAt: Scalars['DateTime'];
   updatedAt: Scalars['DateTime'];
   orderItems: Array<OrderItem>;
+  summary: Array<FulfillmentLineSummary>;
   state: Scalars['String'];
   method: Scalars['String'];
   trackingCode?: Maybe<Scalars['String']>;
   customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+  __typename?: 'FulfillmentLineSummary';
+  orderLine: OrderLine;
+  quantity: Scalars['Int'];
+};
+
 /** Returned when there is an error in transitioning the Fulfillment state */
 export type FulfillmentStateTransitionError = ErrorResult & {
   __typename?: 'FulfillmentStateTransitionError';
@@ -3331,6 +3338,7 @@ export type OrderLine = Node & {
   discounts: Array<Discount>;
   taxLines: Array<TaxLine>;
   order: Order;
+  fulfillments?: Maybe<Array<Fulfillment>>;
   customFields?: Maybe<Scalars['JSON']>;
 };
 

+ 98 - 1
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1571,12 +1571,18 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 /** Returned when there is an error in transitioning the Fulfillment state */
 export type FulfillmentStateTransitionError = ErrorResult & {
     errorCode: ErrorCode;
@@ -3167,6 +3173,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
@@ -6212,7 +6219,15 @@ export type GetOrderFulfillmentsQueryVariables = Exact<{
 export type GetOrderFulfillmentsQuery = {
     order?: Maybe<
         Pick<Order, 'id' | 'state'> & {
-            fulfillments?: Maybe<Array<Pick<Fulfillment, 'id' | 'state' | 'nextStates' | 'method'>>>;
+            fulfillments?: Maybe<
+                Array<
+                    Pick<Fulfillment, 'id' | 'state' | 'nextStates' | 'method'> & {
+                        summary: Array<
+                            Pick<FulfillmentLineSummary, 'quantity'> & { orderLine: Pick<OrderLine, 'id'> }
+                        >;
+                    }
+                >
+            >;
         }
     >;
 };
@@ -6774,6 +6789,32 @@ export type GetOrderWithPaymentsQuery = {
     >;
 };
 
+export type GetOrderLineFulfillmentsQueryVariables = Exact<{
+    id: Scalars['ID'];
+}>;
+
+export type GetOrderLineFulfillmentsQuery = {
+    order?: Maybe<
+        Pick<Order, 'id'> & {
+            lines: Array<
+                Pick<OrderLine, 'id'> & {
+                    fulfillments?: Maybe<
+                        Array<
+                            Pick<Fulfillment, 'id' | 'state'> & {
+                                summary: Array<
+                                    Pick<FulfillmentLineSummary, 'quantity'> & {
+                                        orderLine: Pick<OrderLine, 'id'>;
+                                    }
+                                >;
+                            }
+                        >
+                    >;
+                }
+            >;
+        }
+    >;
+};
+
 export type GetOrderListWithQtyQueryVariables = Exact<{
     options?: Maybe<OrderListOptions>;
 }>;
@@ -8649,6 +8690,22 @@ export namespace GetOrderFulfillments {
     export type Fulfillments = NonNullable<
         NonNullable<NonNullable<GetOrderFulfillmentsQuery['order']>['fulfillments']>[number]
     >;
+    export type Summary = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<NonNullable<GetOrderFulfillmentsQuery['order']>['fulfillments']>[number]
+            >['summary']
+        >[number]
+    >;
+    export type OrderLine = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<
+                    NonNullable<NonNullable<GetOrderFulfillmentsQuery['order']>['fulfillments']>[number]
+                >['summary']
+            >[number]
+        >['orderLine']
+    >;
 }
 
 export namespace GetOrderList {
@@ -9210,6 +9267,46 @@ export namespace GetOrderWithPayments {
     >;
 }
 
+export namespace GetOrderLineFulfillments {
+    export type Variables = GetOrderLineFulfillmentsQueryVariables;
+    export type Query = GetOrderLineFulfillmentsQuery;
+    export type Order = NonNullable<GetOrderLineFulfillmentsQuery['order']>;
+    export type Lines = NonNullable<
+        NonNullable<NonNullable<GetOrderLineFulfillmentsQuery['order']>['lines']>[number]
+    >;
+    export type Fulfillments = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<NonNullable<GetOrderLineFulfillmentsQuery['order']>['lines']>[number]
+            >['fulfillments']
+        >[number]
+    >;
+    export type Summary = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<
+                    NonNullable<
+                        NonNullable<NonNullable<GetOrderLineFulfillmentsQuery['order']>['lines']>[number]
+                    >['fulfillments']
+                >[number]
+            >['summary']
+        >[number]
+    >;
+    export type OrderLine = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<
+                    NonNullable<
+                        NonNullable<
+                            NonNullable<NonNullable<GetOrderLineFulfillmentsQuery['order']>['lines']>[number]
+                        >['fulfillments']
+                    >[number]
+                >['summary']
+            >[number]
+        >['orderLine']
+    >;
+}
+
 export namespace GetOrderListWithQty {
     export type Variables = GetOrderListWithQtyQueryVariables;
     export type Query = GetOrderListWithQtyQuery;

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

@@ -997,12 +997,18 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 export enum GlobalFlag {
     TRUE = 'TRUE',
     FALSE = 'FALSE',
@@ -1962,6 +1968,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 

+ 6 - 0
packages/core/e2e/graphql/shared-definitions.ts

@@ -512,6 +512,12 @@ export const GET_ORDER_FULFILLMENTS = gql`
                 state
                 nextStates
                 method
+                summary {
+                    orderLine {
+                        id
+                    }
+                    quantity
+                }
             }
         }
     }

+ 57 - 1
packages/core/e2e/order.e2e-spec.ts

@@ -47,7 +47,11 @@ import {
     GetOrder,
     GetOrderFulfillmentItems,
     GetOrderFulfillments,
+    GetOrderFulfillmentsQuery,
+    GetOrderFulfillmentsQueryVariables,
     GetOrderHistory,
+    GetOrderLineFulfillmentsQuery,
+    GetOrderLineFulfillmentsQueryVariables,
     GetOrderList,
     GetOrderListFulfillments,
     GetOrderListWithQty,
@@ -756,6 +760,35 @@ describe('Orders resolver', () => {
             ]);
         });
 
+        it('order.fulfillments.summary', async () => {
+            const { order } = await adminClient.query<
+                GetOrderFulfillmentsQuery,
+                GetOrderFulfillmentsQueryVariables
+            >(GET_ORDER_FULFILLMENTS, {
+                id: orderId,
+            });
+
+            expect(order?.fulfillments?.map(pick(['id', 'state', 'summary']))).toEqual([
+                { id: f1Id, state: 'Pending', summary: [{ orderLine: { id: 'T_3' }, quantity: 1 }] },
+                { id: f2Id, state: 'Cancelled', summary: [{ orderLine: { id: 'T_4' }, quantity: 3 }] },
+            ]);
+        });
+
+        it('lines.fulfillments', async () => {
+            const { order } = await adminClient.query<
+                GetOrderLineFulfillmentsQuery,
+                GetOrderLineFulfillmentsQueryVariables
+            >(GET_ORDER_LINE_FULFILLMENTS, {
+                id: orderId,
+            });
+
+            expect(order?.lines.find(l => l.id === 'T_3')!.fulfillments).toEqual([
+                { id: f1Id, state: 'Pending', summary: [{ orderLine: { id: 'T_3' }, quantity: 1 }] },
+            ]);
+            // Cancelled Fulfillments do not appear in the line field
+            expect(order?.lines.find(l => l.id === 'T_4')!.fulfillments).toEqual([]);
+        });
+
         it('creates third fulfillment with same items from second fulfillment', async () => {
             const lines = await getUnfulfilledOrderLineInput(adminClient, orderId);
             const { addFulfillmentToOrder } = await adminClient.query<
@@ -1019,7 +1052,9 @@ describe('Orders resolver', () => {
                 id: orderId,
             });
 
-            expect(order!.fulfillments?.sort(sortById)).toEqual([
+            expect(
+                order!.fulfillments?.sort(sortById).map(pick(['id', 'method', 'state', 'nextStates'])),
+            ).toEqual([
                 { id: f1Id, method: 'Test1', state: 'Delivered', nextStates: ['Cancelled'] },
                 { id: f2Id, method: 'Test2', state: 'Cancelled', nextStates: [] },
                 { id: f3Id, method: 'Test3', state: 'Delivered', nextStates: ['Cancelled'] },
@@ -2662,6 +2697,27 @@ const GET_ORDER_WITH_PAYMENTS = gql`
     }
 `;
 
+export const GET_ORDER_LINE_FULFILLMENTS = gql`
+    query GetOrderLineFulfillments($id: ID!) {
+        order(id: $id) {
+            id
+            lines {
+                id
+                fulfillments {
+                    id
+                    state
+                    summary {
+                        orderLine {
+                            id
+                        }
+                        quantity
+                    }
+                }
+            }
+        }
+    }
+`;
+
 const GET_ORDERS_LIST_WITH_QUANTITIES = gql`
     query GetOrderListWithQty($options: OrderListOptions) {
         orders(options: $options) {

+ 8 - 0
packages/core/src/api/resolvers/entity/fulfillment-entity.resolver.ts

@@ -1,4 +1,5 @@
 import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+import { FulfillmentLineSummary } from '@vendure/payments-plugin/e2e/graphql/generated-admin-types';
 
 import { RequestContextCacheService } from '../../../cache/index';
 import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
@@ -22,6 +23,13 @@ export class FulfillmentEntityResolver {
             () => this.fulfillmentService.getOrderItemsByFulfillmentId(ctx, fulfillment.id),
         );
     }
+
+    @ResolveField()
+    async summary(@Ctx() ctx: RequestContext, @Parent() fulfillment: Fulfillment) {
+        return this.requestContextCache.get(ctx, `FulfillmentEntityResolver.summary(${fulfillment.id})`, () =>
+            this.fulfillmentService.getFulfillmentLineSummary(ctx, fulfillment.id),
+        );
+    }
 }
 
 @Resolver('Fulfillment')

+ 14 - 2
packages/core/src/api/resolvers/entity/order-line-entity.resolver.ts

@@ -1,7 +1,7 @@
 import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
 
-import { Asset, Order, OrderLine, ProductVariant } from '../../../entity';
-import { AssetService, OrderService, ProductVariantService } from '../../../service';
+import { Asset, Fulfillment, Order, OrderLine, ProductVariant } from '../../../entity';
+import { AssetService, FulfillmentService, OrderService, ProductVariantService } from '../../../service';
 import { RequestContext } from '../../common/request-context';
 import { RelationPaths, Relations } from '../../decorators/relations.decorator';
 import { Ctx } from '../../decorators/request-context.decorator';
@@ -12,6 +12,7 @@ export class OrderLineEntityResolver {
         private productVariantService: ProductVariantService,
         private assetService: AssetService,
         private orderService: OrderService,
+        private fulfillmentService: FulfillmentService,
     ) {}
 
     @ResolveField()
@@ -45,4 +46,15 @@ export class OrderLineEntityResolver {
     ): Promise<Order | undefined> {
         return this.orderService.findOneByOrderLineId(ctx, orderLine.id, relations);
     }
+
+    @ResolveField()
+    async fulfillments(
+        @Ctx() ctx: RequestContext,
+        @Parent() orderLine: OrderLine,
+        @Relations(Order) relations: RelationPaths<Order>,
+    ): Promise<Fulfillment[]> {
+        return this.fulfillmentService
+            .getFulfillmentsByOrderLineId(ctx, orderLine.id)
+            .then(results => results.map(r => r.fulfillment));
+    }
 }

+ 7 - 0
packages/core/src/api/schema/common/order.type.graphql

@@ -210,6 +210,7 @@ type OrderLine implements Node {
     discounts: [Discount!]!
     taxLines: [TaxLine!]!
     order: Order!
+    fulfillments: [Fulfillment!]
 }
 
 type Payment implements Node {
@@ -242,11 +243,17 @@ type Refund implements Node {
     metadata: JSON
 }
 
+type FulfillmentLineSummary {
+    orderLine: OrderLine!
+    quantity: Int!
+}
+
 type Fulfillment implements Node {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
     orderItems: [OrderItem!]!
+    summary: [FulfillmentLineSummary!]!
     state: String!
     method: String!
     trackingCode: String

+ 27 - 0
packages/core/src/service/services/fulfillment.service.ts

@@ -3,6 +3,7 @@ import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types'
 import { ID } from '@vendure/common/lib/shared-types';
 import { isObject } from '@vendure/common/lib/shared-utils';
 import { unique } from '@vendure/common/lib/unique';
+import { FulfillmentLineSummary } from '@vendure/payments-plugin/e2e/graphql/generated-admin-types';
 
 import { RequestContext } from '../../api/common/request-context';
 import {
@@ -13,6 +14,7 @@ import {
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Fulfillment } from '../../entity/fulfillment/fulfillment.entity';
+import { OrderLine } from '../../entity/index';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { Order } from '../../entity/order/order.entity';
 import { EventBus } from '../../event-bus/event-bus';
@@ -116,6 +118,31 @@ export class FulfillmentService {
         return fulfillment.orderItems;
     }
 
+    async getFulfillmentLineSummary(
+        ctx: RequestContext,
+        id: ID,
+    ): Promise<Array<{ orderLine: OrderLine; quantity: number }>> {
+        const result = await this.connection
+            .getRepository(ctx, OrderLine)
+            .createQueryBuilder('line')
+            .leftJoinAndSelect('line.items', 'item')
+            .leftJoin('item.fulfillments', 'fulfillment')
+            .select('line.id', 'lineId')
+            .addSelect('COUNT(item.id)', 'itemCount')
+            .groupBy('line.id')
+            .where('fulfillment.id = :id', { id })
+            .getRawMany();
+
+        return Promise.all(
+            result.map(async ({ lineId, itemCount }: { lineId: ID; itemCount: string }) => {
+                return {
+                    orderLine: await this.connection.getEntityOrThrow(ctx, OrderLine, lineId),
+                    quantity: +itemCount,
+                };
+            }),
+        );
+    }
+
     async getFulfillmentsByOrderLineId(
         ctx: RequestContext,
         orderLineId: ID,

+ 7 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1571,12 +1571,18 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 /** Returned when there is an error in transitioning the Fulfillment state */
 export type FulfillmentStateTransitionError = ErrorResult & {
     errorCode: ErrorCode;
@@ -3167,6 +3173,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 

+ 7 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -1571,12 +1571,18 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 /** Returned when there is an error in transitioning the Fulfillment state */
 export type FulfillmentStateTransitionError = ErrorResult & {
     errorCode: ErrorCode;
@@ -3167,6 +3173,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 

+ 7 - 0
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -997,12 +997,18 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 export enum GlobalFlag {
     TRUE = 'TRUE',
     FALSE = 'FALSE',
@@ -1962,6 +1968,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 

+ 8 - 0
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts

@@ -1038,12 +1038,19 @@ export type Fulfillment = Node & {
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
     orderItems: Array<OrderItem>;
+    summary: Array<FulfillmentLineSummary>;
     state: Scalars['String'];
     method: Scalars['String'];
     trackingCode?: Maybe<Scalars['String']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+    __typename?: 'FulfillmentLineSummary';
+    orderLine: OrderLine;
+    quantity: Scalars['Int'];
+};
+
 export enum GlobalFlag {
     TRUE = 'TRUE',
     FALSE = 'FALSE',
@@ -2047,6 +2054,7 @@ export type OrderLine = Node & {
     discounts: Array<Discount>;
     taxLines: Array<TaxLine>;
     order: Order;
+    fulfillments?: Maybe<Array<Fulfillment>>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
schema-admin.json


Fișier diff suprimat deoarece este prea mare
+ 0 - 0
schema-shop.json


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff