diff --git a/packages/core/e2e/order-promotion.e2e-spec.ts b/packages/core/e2e/order-promotion.e2e-spec.ts index 9bad677c16..2eb0f495a7 100644 --- a/packages/core/e2e/order-promotion.e2e-spec.ts +++ b/packages/core/e2e/order-promotion.e2e-spec.ts @@ -1550,8 +1550,8 @@ describe('Promotions applied to Orders', () => { >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE }); orderResultGuard.assertSuccess(applyCouponCode); - expect(applyCouponCode!.totalWithTax).toBe(0); - expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]); + expect(applyCouponCode.totalWithTax).toBe(0); + expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]); await shopClient.query( SET_CUSTOMER, @@ -1565,8 +1565,8 @@ describe('Promotions applied to Orders', () => { ); const { activeOrder } = await shopClient.query(GET_ACTIVE_ORDER); - expect(activeOrder!.couponCodes).toEqual([TEST_COUPON_CODE]); - expect(applyCouponCode!.totalWithTax).toBe(0); + expect(activeOrder.couponCodes).toEqual([TEST_COUPON_CODE]); + expect(applyCouponCode.totalWithTax).toBe(0); }); }); @@ -1755,6 +1755,81 @@ describe('Promotions applied to Orders', () => { expect(applyCouponCode.totalWithTax).toBe(96); }); + // https://github.com/vendure-ecommerce/vendure/issues/2385 + it('prevents negative line price', async () => { + await shopClient.asAnonymousUser(); + const item1000 = getVariantBySlug('item-1000')!; + const couponCode1 = '100%_off'; + const couponCode2 = '100%_off'; + await createPromotion({ + enabled: true, + name: '100% discount ', + couponCode: couponCode1, + conditions: [], + actions: [ + { + code: productsPercentageDiscount.code, + arguments: [ + { name: 'discount', value: '100' }, + { + name: 'productVariantIds', + value: `["${item1000.id}"]`, + }, + ], + }, + ], + }); + await createPromotion({ + enabled: true, + name: '20% discount ', + couponCode: couponCode2, + conditions: [], + actions: [ + { + code: productsPercentageDiscount.code, + arguments: [ + { name: 'discount', value: '20' }, + { + name: 'productVariantIds', + value: `["${item1000.id}"]`, + }, + ], + }, + ], + }); + + await shopClient.query< + CodegenShop.ApplyCouponCodeMutation, + CodegenShop.ApplyCouponCodeMutationVariables + >(APPLY_COUPON_CODE, { couponCode: couponCode1 }); + + await shopClient.query< + CodegenShop.AddItemToOrderMutation, + CodegenShop.AddItemToOrderMutationVariables + >(ADD_ITEM_TO_ORDER, { + productVariantId: item1000.id, + quantity: 1, + }); + + const { activeOrder: check1 } = await shopClient.query( + GET_ACTIVE_ORDER, + ); + + expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0); + expect(check1!.totalWithTax).toBe(0); + + await shopClient.query< + CodegenShop.ApplyCouponCodeMutation, + CodegenShop.ApplyCouponCodeMutationVariables + >(APPLY_COUPON_CODE, { couponCode: couponCode2 }); + + const { activeOrder: check2 } = await shopClient.query( + GET_ACTIVE_ORDER, + ); + expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0); + expect(check2!.totalWithTax).toBe(0); + }); + async function getProducts() { const result = await adminClient.query( GET_PRODUCTS_WITH_VARIANT_PRICES, diff --git a/packages/core/src/entity/order-line/order-line.entity.ts b/packages/core/src/entity/order-line/order-line.entity.ts index 3f424cce84..198d75df8e 100644 --- a/packages/core/src/entity/order-line/order-line.entity.ts +++ b/packages/core/src/entity/order-line/order-line.entity.ts @@ -330,7 +330,16 @@ export class OrderLine extends VendureEntity implements HasCustomFields { } addAdjustment(adjustment: Adjustment) { - this.adjustments = this.adjustments.concat(adjustment); + // We should not allow adding adjustments which would + // result in a negative unit price + const maxDiscount = this.proratedLinePrice * -1; + const limitedAdjustment: Adjustment = { + ...adjustment, + amount: Math.max(maxDiscount, adjustment.amount), + }; + if (limitedAdjustment.amount !== 0) { + this.adjustments = this.adjustments.concat(limitedAdjustment); + } } /**