Skip to content

Commit

Permalink
feat(core): Implement FulfillmentHandlers
Browse files Browse the repository at this point in the history
Relates to #529. This commit introduces a new FulfillmentHandler class which can be used to define
how Fulfillments are created.

BREAKING CHANGE: The Fulfillment and ShippingMethod entities have new fields relating to
FulfillmentHandlers. This will require a DB migration, though no custom data migration will be
needed for this particular change.

The `addFulfillmentToOrder` mutation input has changed: the `method` & `trackingCode` fields
have been replaced by a `handler` field which accepts a FulfillmentHandler code, and any
expected arguments defined by that handler.
  • Loading branch information
michaelbromley committed Nov 26, 2020
1 parent 7efe800 commit 4e53d08
Show file tree
Hide file tree
Showing 37 changed files with 995 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type Query = {
shippingMethod?: Maybe<ShippingMethod>;
shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
shippingCalculators: Array<ConfigurableOperationDefinition>;
fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
testShippingMethod: TestShippingMethodResult;
testEligibleShippingMethods: Array<ShippingMethodQuote>;
taxCategories: Array<TaxCategory>;
Expand Down Expand Up @@ -1218,8 +1219,7 @@ export type UpdateOrderInput = {

export type FulfillOrderInput = {
lines: Array<OrderLineInput>;
method: Scalars['String'];
trackingCode?: Maybe<Scalars['String']>;
handler: ConfigurableOperationInput;
};

export type CancelOrderInput = {
Expand Down Expand Up @@ -1279,6 +1279,19 @@ export type ItemsAlreadyFulfilledError = ErrorResult & {
message: Scalars['String'];
};

/** Returned if the specified FulfillmentHandler code is not valid */
export type InvalidFulfillmentHandlerError = ErrorResult & {
errorCode: ErrorCode;
message: Scalars['String'];
};

/** Returned if an error is thrown in a FulfillmentHandler's createFulfillment method */
export type CreateFulfillmentError = ErrorResult & {
errorCode: ErrorCode;
message: Scalars['String'];
fulfillmentHandlerError: Scalars['String'];
};

/**
* Returned if attempting to create a Fulfillment when there is insufficient
* stockOnHand of a ProductVariant to satisfy the requested quantity.
Expand Down Expand Up @@ -1375,7 +1388,10 @@ export type AddFulfillmentToOrderResult =
| Fulfillment
| EmptyOrderLineSelectionError
| ItemsAlreadyFulfilledError
| InsufficientStockOnHandError;
| InsufficientStockOnHandError
| InvalidFulfillmentHandlerError
| FulfillmentStateTransitionError
| CreateFulfillmentError;

export type CancelOrderResult =
| Order
Expand Down Expand Up @@ -1711,6 +1727,7 @@ export type ShippingMethodTranslationInput = {

export type CreateShippingMethodInput = {
code: Scalars['String'];
fulfillmentHandler: Scalars['String'];
checker: ConfigurableOperationInput;
calculator: ConfigurableOperationInput;
translations: Array<ShippingMethodTranslationInput>;
Expand All @@ -1720,6 +1737,7 @@ export type CreateShippingMethodInput = {
export type UpdateShippingMethodInput = {
id: Scalars['ID'];
code?: Maybe<Scalars['String']>;
fulfillmentHandler?: Maybe<Scalars['String']>;
checker?: Maybe<ConfigurableOperationInput>;
calculator?: Maybe<ConfigurableOperationInput>;
translations: Array<ShippingMethodTranslationInput>;
Expand Down Expand Up @@ -1950,6 +1968,8 @@ export enum ErrorCode {
SETTLE_PAYMENT_ERROR = 'SETTLE_PAYMENT_ERROR',
EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
ITEMS_ALREADY_FULFILLED_ERROR = 'ITEMS_ALREADY_FULFILLED_ERROR',
INVALID_FULFILLMENT_HANDLER_ERROR = 'INVALID_FULFILLMENT_HANDLER_ERROR',
CREATE_FULFILLMENT_ERROR = 'CREATE_FULFILLMENT_ERROR',
INSUFFICIENT_STOCK_ON_HAND_ERROR = 'INSUFFICIENT_STOCK_ON_HAND_ERROR',
MULTIPLE_ORDER_ERROR = 'MULTIPLE_ORDER_ERROR',
CANCEL_ACTIVE_ORDER_ERROR = 'CANCEL_ACTIVE_ORDER_ERROR',
Expand Down Expand Up @@ -3392,6 +3412,7 @@ export type ShippingMethod = Node & {
code: Scalars['String'];
name: Scalars['String'];
description: Scalars['String'];
fulfillmentHandlerCode: Scalars['String'];
checker: ConfigurableOperation;
calculator: ConfigurableOperation;
translations: Array<ShippingMethodTranslation>;
Expand Down Expand Up @@ -3934,6 +3955,7 @@ export type ShippingMethodFilterParameter = {
code?: Maybe<StringOperators>;
name?: Maybe<StringOperators>;
description?: Maybe<StringOperators>;
fulfillmentHandlerCode?: Maybe<StringOperators>;
};

export type ShippingMethodSortParameter = {
Expand All @@ -3943,6 +3965,7 @@ export type ShippingMethodSortParameter = {
code?: Maybe<SortOrder>;
name?: Maybe<SortOrder>;
description?: Maybe<SortOrder>;
fulfillmentHandlerCode?: Maybe<SortOrder>;
};

export type TaxRateFilterParameter = {
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/generated-shop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2428,6 +2428,7 @@ export type ShippingMethod = Node & {
code: Scalars['String'];
name: Scalars['String'];
description: Scalars['String'];
fulfillmentHandlerCode: Scalars['String'];
checker: ConfigurableOperation;
calculator: ConfigurableOperation;
translations: Array<ShippingMethodTranslation>;
Expand Down
28 changes: 25 additions & 3 deletions packages/common/src/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type Query = {
shippingMethod?: Maybe<ShippingMethod>;
shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
shippingCalculators: Array<ConfigurableOperationDefinition>;
fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
testShippingMethod: TestShippingMethodResult;
testEligibleShippingMethods: Array<ShippingMethodQuote>;
taxCategories: Array<TaxCategory>;
Expand Down Expand Up @@ -1368,8 +1369,7 @@ export type UpdateOrderInput = {

export type FulfillOrderInput = {
lines: Array<OrderLineInput>;
method: Scalars['String'];
trackingCode?: Maybe<Scalars['String']>;
handler: ConfigurableOperationInput;
};

export type CancelOrderInput = {
Expand Down Expand Up @@ -1432,6 +1432,21 @@ export type ItemsAlreadyFulfilledError = ErrorResult & {
message: Scalars['String'];
};

/** Returned if the specified FulfillmentHandler code is not valid */
export type InvalidFulfillmentHandlerError = ErrorResult & {
__typename?: 'InvalidFulfillmentHandlerError';
errorCode: ErrorCode;
message: Scalars['String'];
};

/** Returned if an error is thrown in a FulfillmentHandler's createFulfillment method */
export type CreateFulfillmentError = ErrorResult & {
__typename?: 'CreateFulfillmentError';
errorCode: ErrorCode;
message: Scalars['String'];
fulfillmentHandlerError: Scalars['String'];
};

/**
* Returned if attempting to create a Fulfillment when there is insufficient
* stockOnHand of a ProductVariant to satisfy the requested quantity.
Expand Down Expand Up @@ -1531,7 +1546,7 @@ export type TransitionOrderToStateResult = Order | OrderStateTransitionError;

export type SettlePaymentResult = Payment | SettlePaymentError | PaymentStateTransitionError | OrderStateTransitionError;

export type AddFulfillmentToOrderResult = Fulfillment | EmptyOrderLineSelectionError | ItemsAlreadyFulfilledError | InsufficientStockOnHandError;
export type AddFulfillmentToOrderResult = Fulfillment | EmptyOrderLineSelectionError | ItemsAlreadyFulfilledError | InsufficientStockOnHandError | InvalidFulfillmentHandlerError | FulfillmentStateTransitionError | CreateFulfillmentError;

export type CancelOrderResult = Order | EmptyOrderLineSelectionError | QuantityTooGreatError | MultipleOrderError | CancelActiveOrderError | OrderStateTransitionError;

Expand Down Expand Up @@ -1859,6 +1874,7 @@ export type ShippingMethodTranslationInput = {

export type CreateShippingMethodInput = {
code: Scalars['String'];
fulfillmentHandler: Scalars['String'];
checker: ConfigurableOperationInput;
calculator: ConfigurableOperationInput;
translations: Array<ShippingMethodTranslationInput>;
Expand All @@ -1868,6 +1884,7 @@ export type CreateShippingMethodInput = {
export type UpdateShippingMethodInput = {
id: Scalars['ID'];
code?: Maybe<Scalars['String']>;
fulfillmentHandler?: Maybe<Scalars['String']>;
checker?: Maybe<ConfigurableOperationInput>;
calculator?: Maybe<ConfigurableOperationInput>;
translations: Array<ShippingMethodTranslationInput>;
Expand Down Expand Up @@ -2109,6 +2126,8 @@ export enum ErrorCode {
SETTLE_PAYMENT_ERROR = 'SETTLE_PAYMENT_ERROR',
EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
ITEMS_ALREADY_FULFILLED_ERROR = 'ITEMS_ALREADY_FULFILLED_ERROR',
INVALID_FULFILLMENT_HANDLER_ERROR = 'INVALID_FULFILLMENT_HANDLER_ERROR',
CREATE_FULFILLMENT_ERROR = 'CREATE_FULFILLMENT_ERROR',
INSUFFICIENT_STOCK_ON_HAND_ERROR = 'INSUFFICIENT_STOCK_ON_HAND_ERROR',
MULTIPLE_ORDER_ERROR = 'MULTIPLE_ORDER_ERROR',
CANCEL_ACTIVE_ORDER_ERROR = 'CANCEL_ACTIVE_ORDER_ERROR',
Expand Down Expand Up @@ -3615,6 +3634,7 @@ export type ShippingMethod = Node & {
code: Scalars['String'];
name: Scalars['String'];
description: Scalars['String'];
fulfillmentHandlerCode: Scalars['String'];
checker: ConfigurableOperation;
calculator: ConfigurableOperation;
translations: Array<ShippingMethodTranslation>;
Expand Down Expand Up @@ -4166,6 +4186,7 @@ export type ShippingMethodFilterParameter = {
code?: Maybe<StringOperators>;
name?: Maybe<StringOperators>;
description?: Maybe<StringOperators>;
fulfillmentHandlerCode?: Maybe<StringOperators>;
};

export type ShippingMethodSortParameter = {
Expand All @@ -4175,6 +4196,7 @@ export type ShippingMethodSortParameter = {
code?: Maybe<SortOrder>;
name?: Maybe<SortOrder>;
description?: Maybe<SortOrder>;
fulfillmentHandlerCode?: Maybe<SortOrder>;
};

export type TaxRateFilterParameter = {
Expand Down
50 changes: 31 additions & 19 deletions packages/core/e2e/fulfillment-process.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
/* tslint:disable:no-non-null-assertion */
import { CustomFulfillmentProcess, FulfillmentState, mergeConfig } from '@vendure/core';
import { createTestEnvironment } from '@vendure/testing';
import gql from 'graphql-tag';
import { CustomFulfillmentProcess, manualFulfillmentHandler, mergeConfig } from '@vendure/core';
import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
import path from 'path';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';

import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
import {
CreateFulfillment,
ErrorCode,
FulfillmentFragment,
GetCustomerList,
GetOrderFulfillments,
TransitFulfillment,
Expand All @@ -31,6 +32,9 @@ const transitionEndSpy2 = jest.fn();
const transitionErrorSpy = jest.fn();

describe('Fulfillment process', () => {
const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard(
input => !!input.id,
);
const VALIDATION_ERROR_MESSAGE = 'Fulfillment must have a tracking code';
const customOrderProcess: CustomFulfillmentProcess<'AwaitingPickup'> = {
init(injector) {
Expand Down Expand Up @@ -124,16 +128,24 @@ describe('Fulfillment process', () => {
await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
input: {
lines: [{ orderLineId: 'T_1', quantity: 1 }],
method: 'Test1',
handler: {
code: manualFulfillmentHandler.code,
arguments: [{ name: 'method', value: 'Test1' }],
},
},
});

// Add a fulfillment with tracking code
await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(CREATE_FULFILLMENT, {
input: {
lines: [{ orderLineId: 'T_2', quantity: 1 }],
method: 'Test1',
trackingCode: '222',
handler: {
code: manualFulfillmentHandler.code,
arguments: [
{ name: 'method', value: 'Test1' },
{ name: 'trackingCode', value: '222' },
],
},
},
});
}, TEST_SETUP_TIMEOUT_MS);
Expand Down Expand Up @@ -168,18 +180,17 @@ describe('Fulfillment process', () => {
transitionErrorSpy.mockClear();
transitionEndSpy.mockClear();

try {
await adminClient.query<TransitFulfillment.Mutation, TransitFulfillment.Variables>(
TRANSIT_FULFILLMENT,
{
id: 'T_1',
state: 'Shipped',
},
);
fail('Should have thrown');
} catch (e) {
expect(e.message).toContain(VALIDATION_ERROR_MESSAGE);
}
const { transitionFulfillmentToState } = await adminClient.query<
TransitFulfillment.Mutation,
TransitFulfillment.Variables
>(TRANSIT_FULFILLMENT, {
id: 'T_1',
state: 'Shipped',
});

fulfillmentGuard.assertErrorResult(transitionFulfillmentToState);
expect(transitionFulfillmentToState.errorCode).toBe(ErrorCode.FULFILLMENT_STATE_TRANSITION_ERROR);
expect(transitionFulfillmentToState.transitionError).toBe(VALIDATION_ERROR_MESSAGE);

expect(transitionStartSpy).toHaveBeenCalledTimes(1);
expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -212,6 +223,7 @@ describe('Fulfillment process', () => {
id: 'T_2',
state: 'Shipped',
});
fulfillmentGuard.assertSuccess(transitionFulfillmentToState);

expect(transitionEndSpy).toHaveBeenCalledTimes(1);
expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AwaitingPickup', 'Shipped']);
Expand Down
Loading

0 comments on commit 4e53d08

Please sign in to comment.