-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): Implement ChangedPriceHandlingStrategy
Relates to #664 BREAKING CHANGE: The OrderItem entity has a new field, `initialListPrice`, used to better handle price changes to items in an active Order. This schema change will require a DB migration.
- Loading branch information
1 parent
e4d3aed
commit 3aae4fb
Showing
20 changed files
with
385 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
217 changes: 217 additions & 0 deletions
217
packages/core/e2e/order-changed-price-handling.e2e-spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
/* tslint:disable:no-non-null-assertion */ | ||
import { | ||
ChangedPriceHandlingStrategy, | ||
mergeConfig, | ||
OrderItem, | ||
PriceCalculationResult, | ||
RequestContext, | ||
} from '@vendure/core'; | ||
import { createTestEnvironment } from '@vendure/testing'; | ||
import path from 'path'; | ||
|
||
import { initialData } from '../../../e2e-common/e2e-initial-data'; | ||
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config'; | ||
|
||
import { UpdateProductVariants } from './graphql/generated-e2e-admin-types'; | ||
import { AddItemToOrder, AdjustItemQuantity, GetActiveOrder } from './graphql/generated-e2e-shop-types'; | ||
import { UPDATE_PRODUCT_VARIANTS } from './graphql/shared-definitions'; | ||
import { ADD_ITEM_TO_ORDER, ADJUST_ITEM_QUANTITY, GET_ACTIVE_ORDER } from './graphql/shop-definitions'; | ||
|
||
class TestChangedPriceStrategy implements ChangedPriceHandlingStrategy { | ||
static spy = jest.fn(); | ||
static useLatestPrice = true; | ||
|
||
handlePriceChange( | ||
ctx: RequestContext, | ||
current: PriceCalculationResult, | ||
existingItems: OrderItem[], | ||
): PriceCalculationResult { | ||
TestChangedPriceStrategy.spy(current); | ||
if (TestChangedPriceStrategy.useLatestPrice) { | ||
return current; | ||
} else { | ||
return { | ||
price: existingItems[0].listPrice, | ||
priceIncludesTax: existingItems[0].listPriceIncludesTax, | ||
}; | ||
} | ||
} | ||
} | ||
|
||
describe('ChangedPriceHandlingStrategy', () => { | ||
const { server, shopClient, adminClient } = createTestEnvironment( | ||
mergeConfig(testConfig, { | ||
orderOptions: { | ||
changedPriceHandlingStrategy: new TestChangedPriceStrategy(), | ||
}, | ||
}), | ||
); | ||
|
||
beforeAll(async () => { | ||
await server.init({ | ||
initialData, | ||
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'), | ||
customerCount: 1, | ||
}); | ||
await adminClient.asSuperAdmin(); | ||
}, TEST_SETUP_TIMEOUT_MS); | ||
|
||
afterAll(async () => { | ||
await server.destroy(); | ||
}); | ||
|
||
it('unitPriceChangeSinceAdded starts as 0', async () => { | ||
TestChangedPriceStrategy.spy.mockClear(); | ||
|
||
await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, { | ||
productVariantId: 'T_12', | ||
quantity: 1, | ||
}); | ||
|
||
const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER); | ||
|
||
expect(activeOrder?.lines[0].unitPriceChangeSinceAdded).toBe(0); | ||
expect(activeOrder?.lines[0].unitPrice).toBe(5374); | ||
expect(TestChangedPriceStrategy.spy).not.toHaveBeenCalled(); | ||
}); | ||
|
||
describe('use latest price', () => { | ||
let firstOrderLineId: string; | ||
|
||
beforeAll(() => { | ||
TestChangedPriceStrategy.useLatestPrice = true; | ||
}); | ||
|
||
it('calls handlePriceChange on addItemToOrder', async () => { | ||
TestChangedPriceStrategy.spy.mockClear(); | ||
|
||
await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>( | ||
UPDATE_PRODUCT_VARIANTS, | ||
{ | ||
input: [ | ||
{ | ||
id: 'T_12', | ||
price: 6000, | ||
}, | ||
], | ||
}, | ||
); | ||
|
||
await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, { | ||
productVariantId: 'T_12', | ||
quantity: 1, | ||
}); | ||
|
||
const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER); | ||
expect(activeOrder?.lines[0].unitPriceChangeSinceAdded).toBe(626); | ||
expect(activeOrder?.lines[0].unitPrice).toBe(6000); | ||
expect(activeOrder?.lines[0].items.every(i => i.unitPrice === 6000)).toBe(true); | ||
expect(TestChangedPriceStrategy.spy).toHaveBeenCalledTimes(1); | ||
|
||
firstOrderLineId = activeOrder!.lines[0].id; | ||
}); | ||
|
||
it('calls handlePriceChange on adjustOrderLine', async () => { | ||
TestChangedPriceStrategy.spy.mockClear(); | ||
|
||
await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>( | ||
UPDATE_PRODUCT_VARIANTS, | ||
{ | ||
input: [ | ||
{ | ||
id: 'T_12', | ||
price: 3000, | ||
}, | ||
], | ||
}, | ||
); | ||
|
||
await shopClient.query<AdjustItemQuantity.Mutation, AdjustItemQuantity.Variables>( | ||
ADJUST_ITEM_QUANTITY, | ||
{ | ||
orderLineId: firstOrderLineId, | ||
quantity: 3, | ||
}, | ||
); | ||
|
||
const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER); | ||
expect(activeOrder?.lines[0].unitPriceChangeSinceAdded).toBe(-2374); | ||
expect(activeOrder?.lines[0].unitPrice).toBe(3000); | ||
expect(activeOrder?.lines[0].items.every(i => i.unitPrice === 3000)).toBe(true); | ||
expect(TestChangedPriceStrategy.spy).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('use original price', () => { | ||
let secondOrderLineId: string; | ||
const ORIGINAL_PRICE = 7896; | ||
|
||
beforeAll(() => { | ||
TestChangedPriceStrategy.useLatestPrice = false; | ||
}); | ||
|
||
it('calls handlePriceChange on addItemToOrder', async () => { | ||
TestChangedPriceStrategy.spy.mockClear(); | ||
|
||
await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, { | ||
productVariantId: 'T_13', | ||
quantity: 1, | ||
}); | ||
|
||
await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>( | ||
UPDATE_PRODUCT_VARIANTS, | ||
{ | ||
input: [ | ||
{ | ||
id: 'T_13', | ||
price: 8000, | ||
}, | ||
], | ||
}, | ||
); | ||
|
||
await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, { | ||
productVariantId: 'T_13', | ||
quantity: 1, | ||
}); | ||
|
||
const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER); | ||
expect(activeOrder?.lines[1].unitPriceChangeSinceAdded).toBe(0); | ||
expect(activeOrder?.lines[1].unitPrice).toBe(ORIGINAL_PRICE); | ||
expect(activeOrder?.lines[1].items.every(i => i.unitPrice === ORIGINAL_PRICE)).toBe(true); | ||
expect(TestChangedPriceStrategy.spy).toHaveBeenCalledTimes(1); | ||
|
||
secondOrderLineId = activeOrder!.lines[1].id; | ||
}); | ||
|
||
it('calls handlePriceChange on adjustOrderLine', async () => { | ||
TestChangedPriceStrategy.spy.mockClear(); | ||
|
||
await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>( | ||
UPDATE_PRODUCT_VARIANTS, | ||
{ | ||
input: [ | ||
{ | ||
id: 'T_13', | ||
price: 3000, | ||
}, | ||
], | ||
}, | ||
); | ||
|
||
await shopClient.query<AdjustItemQuantity.Mutation, AdjustItemQuantity.Variables>( | ||
ADJUST_ITEM_QUANTITY, | ||
{ | ||
orderLineId: secondOrderLineId, | ||
quantity: 3, | ||
}, | ||
); | ||
|
||
const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER); | ||
expect(activeOrder?.lines[1].unitPriceChangeSinceAdded).toBe(0); | ||
expect(activeOrder?.lines[1].unitPrice).toBe(ORIGINAL_PRICE); | ||
expect(activeOrder?.lines[1].items.every(i => i.unitPrice === ORIGINAL_PRICE)).toBe(true); | ||
expect(TestChangedPriceStrategy.spy).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
packages/core/src/config/order/changed-price-handling-strategy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { RequestContext } from '../../api/common/request-context'; | ||
import { PriceCalculationResult } from '../../common/types/common-types'; | ||
import { InjectableStrategy } from '../../common/types/injectable-strategy'; | ||
import { OrderItem } from '../../entity/order-item/order-item.entity'; | ||
|
||
/** | ||
* @description | ||
* This strategy defines how we handle the situation where an OrderItem exists in an Order, and | ||
* then later on another is added but in the mean time the price of the ProductVariant has changed. | ||
* | ||
* By default, the latest price will be used. Any price changes resulting from using a newer price | ||
* will be reflected in the GraphQL `OrderLine.unitPrice[WithTax]ChangeSinceAdded` field. | ||
* | ||
* @docsCategory orders | ||
*/ | ||
export interface ChangedPriceHandlingStrategy extends InjectableStrategy { | ||
/** | ||
* @description | ||
* This method is called when adding to or adjusting OrderLines, if the latest price | ||
* (as determined by the ProductVariant price, potentially modified by the configured | ||
* {@link OrderItemPriceCalculationStrategy}) differs from the initial price at the time | ||
* that the OrderLine was created. | ||
*/ | ||
handlePriceChange( | ||
ctx: RequestContext, | ||
current: PriceCalculationResult, | ||
existingItems: OrderItem[], | ||
): PriceCalculationResult | Promise<PriceCalculationResult>; | ||
} |
Oops, something went wrong.