/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ErrorCode } from '@vendure/common/lib/generated-types'; import { CustomOrderProcess, defaultOrderProcess, mergeConfig, TransactionalConnection } from '@vendure/core'; import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; import path from 'path'; import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { initialData } from '../../../e2e-common/e2e-initial-data'; import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods'; import { orderFragment } from './graphql/fragments-admin'; import { FragmentOf } from './graphql/graphql-admin'; import { FragmentOf as FragmentOfShop } from './graphql/graphql-shop'; import { adminTransitionToStateDocument, getOrderDocument } from './graphql/shared-definitions'; import { addItemToOrderDocument, addPaymentDocument, getNextStatesDocument, setCustomerDocument, setShippingAddressDocument, setShippingMethodDocument, testOrderFragment, transitionToStateDocument, updatedOrderFragment, } from './graphql/shop-definitions'; const initSpy = vi.fn(); const transitionStartSpy = vi.fn(); const transitionEndSpy = vi.fn(); const transitionEndSpy2 = vi.fn(); const transitionErrorSpy = vi.fn(); describe('Order process', () => { const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address'; const customOrderProcess: CustomOrderProcess<'ValidatingCustomer' | 'PaymentProcessing'> = { init(injector) { initSpy(injector.get(TransactionalConnection).rawConnection.name); }, transitions: { AddingItems: { to: ['ValidatingCustomer'], mergeStrategy: 'replace', }, ValidatingCustomer: { to: ['ArrangingPayment', 'AddingItems'], }, ArrangingPayment: { to: ['PaymentProcessing'], }, PaymentProcessing: { to: ['PaymentAuthorized', 'PaymentSettled'], }, }, onTransitionStart(fromState, toState, data) { transitionStartSpy(fromState, toState, data); if (toState === 'ValidatingCustomer') { if (!data.order.customer) { return false; } if (!data.order.customer.emailAddress.includes('@company.com')) { return VALIDATION_ERROR_MESSAGE; } } }, onTransitionEnd(fromState, toState, data) { transitionEndSpy(fromState, toState, data); }, onTransitionError(fromState, toState, message) { transitionErrorSpy(fromState, toState, message); }, }; const customOrderProcess2: CustomOrderProcess<'ValidatingCustomer'> = { transitions: { ValidatingCustomer: { to: ['Cancelled'], }, }, onTransitionEnd(fromState, toState, data) { transitionEndSpy2(fromState, toState, data); }, }; // Create guards for different fragment types type TestOrderFragmentType = FragmentOfShop; type UpdatedOrderFragmentType = FragmentOfShop; type AdminOrderFragmentType = FragmentOf; const testOrderGuard: ErrorResultGuard = createErrorResultGuard( input => !!input.lines, ); const updatedOrderGuard: ErrorResultGuard = createErrorResultGuard( input => !!input.lines, ); const adminOrderGuard: ErrorResultGuard = createErrorResultGuard( input => !!input.id, ); const { server, adminClient, shopClient } = createTestEnvironment( mergeConfig(testConfig(), { orderOptions: { process: [defaultOrderProcess, customOrderProcess, customOrderProcess2] as any }, paymentOptions: { paymentMethodHandlers: [testSuccessfulPaymentMethod], }, }), ); beforeAll(async () => { await server.init({ initialData: { ...initialData, paymentMethods: [ { name: testSuccessfulPaymentMethod.code, handler: { code: testSuccessfulPaymentMethod.code, arguments: [] }, }, ], }, productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'), customerCount: 1, }); await adminClient.asSuperAdmin(); }, TEST_SETUP_TIMEOUT_MS); afterAll(async () => { await server.destroy(); }); describe('Initial transition', () => { it('transitions from Created to AddingItems on creation', async () => { transitionStartSpy.mockClear(); transitionEndSpy.mockClear(); await shopClient.asAnonymousUser(); await shopClient.query(addItemToOrderDocument, { productVariantId: 'T_1', quantity: 1, }); expect(transitionStartSpy).toHaveBeenCalledTimes(1); expect(transitionEndSpy).toHaveBeenCalledTimes(1); expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']); expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']); }); }); describe('CustomOrderProcess', () => { it('CustomOrderProcess is injectable', () => { expect(initSpy).toHaveBeenCalled(); expect(initSpy.mock.calls[0][0]).toBe('default'); }); it('replaced transition target', async () => { await shopClient.query(addItemToOrderDocument, { productVariantId: 'T_1', quantity: 1, }); const { nextOrderStates } = await shopClient.query(getNextStatesDocument); expect(nextOrderStates).toEqual(['ValidatingCustomer']); }); it('custom onTransitionStart handler returning false', async () => { transitionStartSpy.mockClear(); transitionEndSpy.mockClear(); const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, { state: 'ValidatingCustomer', }); testOrderGuard.assertSuccess(transitionOrderToState!); expect(transitionStartSpy).toHaveBeenCalledTimes(1); expect(transitionEndSpy).not.toHaveBeenCalled(); expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual([ 'AddingItems', 'ValidatingCustomer', ]); expect(transitionOrderToState.state).toBe('AddingItems'); }); it('custom onTransitionStart handler returning error message', async () => { transitionStartSpy.mockClear(); transitionErrorSpy.mockClear(); await shopClient.query(setCustomerDocument, { input: { firstName: 'Joe', lastName: 'Test', emailAddress: 'joetest@gmail.com', }, }); const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, { state: 'ValidatingCustomer', }); testOrderGuard.assertErrorResult(transitionOrderToState!); expect(transitionOrderToState.message).toBe( 'Cannot transition Order from "AddingItems" to "ValidatingCustomer"', ); expect(transitionOrderToState.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR); expect(transitionOrderToState.transitionError).toBe(VALIDATION_ERROR_MESSAGE); expect(transitionOrderToState.fromState).toBe('AddingItems'); expect(transitionOrderToState.toState).toBe('ValidatingCustomer'); expect(transitionStartSpy).toHaveBeenCalledTimes(1); expect(transitionErrorSpy).toHaveBeenCalledTimes(1); expect(transitionEndSpy).not.toHaveBeenCalled(); expect(transitionErrorSpy.mock.calls[0]).toEqual([ 'AddingItems', 'ValidatingCustomer', VALIDATION_ERROR_MESSAGE, ]); }); it('custom onTransitionStart handler allows transition', async () => { transitionEndSpy.mockClear(); await shopClient.query(setCustomerDocument, { input: { firstName: 'Joe', lastName: 'Test', emailAddress: 'joetest@company.com', }, }); const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, { state: 'ValidatingCustomer', }); testOrderGuard.assertSuccess(transitionOrderToState!); expect(transitionEndSpy).toHaveBeenCalledTimes(1); expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AddingItems', 'ValidatingCustomer']); expect(transitionOrderToState.state).toBe('ValidatingCustomer'); }); it('composes multiple CustomOrderProcesses', async () => { transitionEndSpy.mockClear(); transitionEndSpy2.mockClear(); const { nextOrderStates } = await shopClient.query(getNextStatesDocument); expect(nextOrderStates).toEqual(['ArrangingPayment', 'AddingItems', 'Cancelled']); await shopClient.query(transitionToStateDocument, { state: 'AddingItems', }); expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['ValidatingCustomer', 'AddingItems']); expect(transitionEndSpy2.mock.calls[0].slice(0, 2)).toEqual([ 'ValidatingCustomer', 'AddingItems', ]); }); // https://github.com/vendure-ecommerce/vendure/issues/963 it('allows addPaymentToOrder from a custom state', async () => { await shopClient.query(setShippingMethodDocument, { id: ['T_1'] }); const result0 = await shopClient.query(transitionToStateDocument, { state: 'ValidatingCustomer', }); testOrderGuard.assertSuccess(result0.transitionOrderToState!); const result1 = await shopClient.query(transitionToStateDocument, { state: 'ArrangingPayment', }); testOrderGuard.assertSuccess(result1.transitionOrderToState!); const result2 = await shopClient.query(transitionToStateDocument, { state: 'PaymentProcessing', }); testOrderGuard.assertSuccess(result2.transitionOrderToState!); expect(result2.transitionOrderToState.state).toBe('PaymentProcessing'); const { addPaymentToOrder } = await shopClient.query(addPaymentDocument, { input: { method: testSuccessfulPaymentMethod.code, metadata: {}, }, }); updatedOrderGuard.assertSuccess(addPaymentToOrder); expect(addPaymentToOrder.state).toBe('PaymentSettled'); }); }); describe('Admin API transition constraints', () => { let order: FragmentOfShop; beforeAll(async () => { await shopClient.asAnonymousUser(); await shopClient.query(addItemToOrderDocument, { productVariantId: 'T_1', quantity: 1, }); await shopClient.query(setCustomerDocument, { input: { firstName: 'Su', lastName: 'Test', emailAddress: 'sutest@company.com', }, }); await shopClient.query(setShippingAddressDocument, { input: { fullName: 'name', streetLine1: '12 the street', city: 'foo', postalCode: '123456', countryCode: 'US', phoneNumber: '4444444', }, }); await shopClient.query(setShippingMethodDocument, { id: ['T_1'] }); await shopClient.query(transitionToStateDocument, { state: 'ValidatingCustomer', }); const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, { state: 'ArrangingPayment', }); testOrderGuard.assertSuccess(transitionOrderToState!); order = transitionOrderToState as TestOrderFragmentType; }); it('cannot manually transition to PaymentAuthorized', async () => { expect(order.state).toBe('ArrangingPayment'); const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, { id: order.id, state: 'PaymentAuthorized', }); adminOrderGuard.assertErrorResult(transitionOrderToState!); expect(transitionOrderToState.message).toBe( 'Cannot transition Order from "ArrangingPayment" to "PaymentAuthorized"', ); expect(transitionOrderToState.transitionError).toBe( 'Cannot transition Order to the "PaymentAuthorized" state when the total is not covered by authorized Payments', ); const result = await adminClient.query(getOrderDocument, { id: order.id, }); expect(result.order?.state).toBe('ArrangingPayment'); }); it('cannot manually transition to PaymentSettled', async () => { const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, { id: order.id, state: 'PaymentSettled', }); adminOrderGuard.assertErrorResult(transitionOrderToState!); expect(transitionOrderToState.message).toBe( 'Cannot transition Order from "ArrangingPayment" to "PaymentSettled"', ); expect(transitionOrderToState.transitionError).toContain( 'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments', ); const result = await adminClient.query(getOrderDocument, { id: order.id, }); expect(result.order?.state).toBe('ArrangingPayment'); }); it('cannot manually transition to Cancelled', async () => { const { addPaymentToOrder } = await shopClient.query(addPaymentDocument, { input: { method: testSuccessfulPaymentMethod.code, metadata: {}, }, }); updatedOrderGuard.assertSuccess(addPaymentToOrder); expect(addPaymentToOrder?.state).toBe('PaymentSettled'); const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, { id: order.id, state: 'Cancelled', }); adminOrderGuard.assertErrorResult(transitionOrderToState!); expect(transitionOrderToState.message).toBe( 'Cannot transition Order from "PaymentSettled" to "Cancelled"', ); expect(transitionOrderToState.transitionError).toContain( 'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled', ); const result = await adminClient.query(getOrderDocument, { id: order.id, }); expect(result.order?.state).toBe('PaymentSettled'); }); it('cannot manually transition to PartiallyDelivered', async () => { const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, { id: order.id, state: 'PartiallyDelivered', }); adminOrderGuard.assertErrorResult(transitionOrderToState!); expect(transitionOrderToState.message).toBe( 'Cannot transition Order from "PaymentSettled" to "PartiallyDelivered"', ); expect(transitionOrderToState.transitionError).toContain( 'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered', ); const result = await adminClient.query(getOrderDocument, { id: order.id, }); expect(result.order?.state).toBe('PaymentSettled'); }); it('cannot manually transition to Delivered', async () => { const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, { id: order.id, state: 'Delivered', }); adminOrderGuard.assertErrorResult(transitionOrderToState!); expect(transitionOrderToState.message).toBe( 'Cannot transition Order from "PaymentSettled" to "Delivered"', ); expect(transitionOrderToState.transitionError).toContain( 'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered', ); const result = await adminClient.query(getOrderDocument, { id: order.id, }); expect(result.order?.state).toBe('PaymentSettled'); }); }); });