فهرست منبع

feat(core): Split taxes from adjustments

Relates to #573

BREAKING CHANGE: The `OrderLine.pendingAdjustments` field has been renamed to `adjustments`, tax
adjustments are now stored in a new field, `taxLines`. This will require a DB migration to
preserve data from existing Orders (see guide in release blog post)
Michael Bromley 5 سال پیش
والد
کامیت
2c71a82a40
25فایلهای تغییر یافته به همراه219 افزوده شده و 132 حذف شده
  1. 10 7
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 8 6
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  3. 9 6
      packages/common/src/generated-shop-types.ts
  4. 10 7
      packages/common/src/generated-types.ts
  5. 8 6
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  6. 18 8
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  7. 8 0
      packages/core/e2e/graphql/shop-definitions.ts
  8. 52 10
      packages/core/e2e/order-promotion.e2e-spec.ts
  9. 10 6
      packages/core/e2e/order-taxes.e2e-spec.ts
  10. 6 6
      packages/core/src/api/schema/common/common-types.graphql
  11. 2 0
      packages/core/src/api/schema/type/order.type.graphql
  12. 19 0
      packages/core/src/config/promotion/actions/order-fixed-discount-action.ts
  13. 2 0
      packages/core/src/config/promotion/index.ts
  14. 1 0
      packages/core/src/config/shipping-method/shipping-calculator.ts
  15. 18 33
      packages/core/src/entity/order-item/order-item.entity.ts
  16. 6 1
      packages/core/src/entity/order-line/order-line.entity.ts
  17. 5 8
      packages/core/src/entity/tax-rate/tax-rate.entity.ts
  18. 5 3
      packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts
  19. 6 11
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  20. 2 2
      packages/core/src/service/services/order-testing.service.ts
  21. 2 2
      packages/core/src/service/services/order.service.ts
  22. 8 6
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  23. 4 4
      packages/email-plugin/src/mock-events.ts
  24. 0 0
      schema-admin.json
  25. 0 0
      schema-shop.json

+ 10 - 7
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2000,13 +2000,7 @@ export enum GlobalFlag {
 }
 
 export enum AdjustmentType {
-  TAX = 'TAX',
-  PROMOTION = 'PROMOTION',
-  SHIPPING = 'SHIPPING',
-  REFUND = 'REFUND',
-  TAX_REFUND = 'TAX_REFUND',
-  PROMOTION_REFUND = 'PROMOTION_REFUND',
-  SHIPPING_REFUND = 'SHIPPING_REFUND'
+  PROMOTION = 'PROMOTION'
 }
 
 export type Adjustment = {
@@ -2017,6 +2011,13 @@ export type Adjustment = {
   amount: Scalars['Int'];
 };
 
+export type TaxLine = {
+  __typename?: 'TaxLine';
+  description: Scalars['String'];
+  amount: Scalars['Int'];
+  taxRate: Scalars['Float'];
+};
+
 export type ConfigArg = {
   __typename?: 'ConfigArg';
   name: Scalars['String'];
@@ -3432,6 +3433,7 @@ export type OrderItem = Node & {
   unitPriceIncludesTax: Scalars['Boolean'];
   taxRate: Scalars['Float'];
   adjustments: Array<Adjustment>;
+  taxLines: Array<TaxLine>;
   fulfillment?: Maybe<Fulfillment>;
   refundId?: Maybe<Scalars['ID']>;
 };
@@ -3457,6 +3459,7 @@ export type OrderLine = Node & {
   /** The total price of the line including tax */
   linePriceWithTax: Scalars['Int'];
   adjustments: Array<Adjustment>;
+  taxLines: Array<TaxLine>;
   order: Order;
   customFields?: Maybe<Scalars['JSON']>;
 };

+ 8 - 6
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1817,13 +1817,7 @@ export enum GlobalFlag {
 }
 
 export enum AdjustmentType {
-    TAX = 'TAX',
     PROMOTION = 'PROMOTION',
-    SHIPPING = 'SHIPPING',
-    REFUND = 'REFUND',
-    TAX_REFUND = 'TAX_REFUND',
-    PROMOTION_REFUND = 'PROMOTION_REFUND',
-    SHIPPING_REFUND = 'SHIPPING_REFUND',
 }
 
 export type Adjustment = {
@@ -1833,6 +1827,12 @@ export type Adjustment = {
     amount: Scalars['Int'];
 };
 
+export type TaxLine = {
+    description: Scalars['String'];
+    amount: Scalars['Int'];
+    taxRate: Scalars['Float'];
+};
+
 export type ConfigArg = {
     name: Scalars['String'];
     value: Scalars['String'];
@@ -3200,6 +3200,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     fulfillment?: Maybe<Fulfillment>;
     refundId?: Maybe<Scalars['ID']>;
 };
@@ -3224,6 +3225,7 @@ export type OrderLine = Node & {
     /** The total price of the line including tax */
     linePriceWithTax: Scalars['Int'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     order: Order;
     customFields?: Maybe<Scalars['JSON']>;
 };

+ 9 - 6
packages/common/src/generated-shop-types.ts

@@ -298,13 +298,7 @@ export enum GlobalFlag {
 }
 
 export enum AdjustmentType {
-    TAX = 'TAX',
     PROMOTION = 'PROMOTION',
-    SHIPPING = 'SHIPPING',
-    REFUND = 'REFUND',
-    TAX_REFUND = 'TAX_REFUND',
-    PROMOTION_REFUND = 'PROMOTION_REFUND',
-    SHIPPING_REFUND = 'SHIPPING_REFUND',
 }
 
 export type Adjustment = {
@@ -315,6 +309,13 @@ export type Adjustment = {
     amount: Scalars['Int'];
 };
 
+export type TaxLine = {
+    __typename?: 'TaxLine';
+    description: Scalars['String'];
+    amount: Scalars['Int'];
+    taxRate: Scalars['Float'];
+};
+
 export type ConfigArg = {
     __typename?: 'ConfigArg';
     name: Scalars['String'];
@@ -2113,6 +2114,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     fulfillment?: Maybe<Fulfillment>;
     refundId?: Maybe<Scalars['ID']>;
 };
@@ -2138,6 +2140,7 @@ export type OrderLine = Node & {
     /** The total price of the line including tax */
     linePriceWithTax: Scalars['Int'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     order: Order;
     customFields?: Maybe<Scalars['JSON']>;
 };

+ 10 - 7
packages/common/src/generated-types.ts

@@ -1969,13 +1969,7 @@ export enum GlobalFlag {
 }
 
 export enum AdjustmentType {
-  TAX = 'TAX',
-  PROMOTION = 'PROMOTION',
-  SHIPPING = 'SHIPPING',
-  REFUND = 'REFUND',
-  TAX_REFUND = 'TAX_REFUND',
-  PROMOTION_REFUND = 'PROMOTION_REFUND',
-  SHIPPING_REFUND = 'SHIPPING_REFUND'
+  PROMOTION = 'PROMOTION'
 }
 
 export type Adjustment = {
@@ -1986,6 +1980,13 @@ export type Adjustment = {
   amount: Scalars['Int'];
 };
 
+export type TaxLine = {
+  __typename?: 'TaxLine';
+  description: Scalars['String'];
+  amount: Scalars['Int'];
+  taxRate: Scalars['Float'];
+};
+
 export type ConfigArg = {
   __typename?: 'ConfigArg';
   name: Scalars['String'];
@@ -3400,6 +3401,7 @@ export type OrderItem = Node & {
   unitPriceIncludesTax: Scalars['Boolean'];
   taxRate: Scalars['Float'];
   adjustments: Array<Adjustment>;
+  taxLines: Array<TaxLine>;
   fulfillment?: Maybe<Fulfillment>;
   refundId?: Maybe<Scalars['ID']>;
 };
@@ -3425,6 +3427,7 @@ export type OrderLine = Node & {
   /** The total price of the line including tax */
   linePriceWithTax: Scalars['Int'];
   adjustments: Array<Adjustment>;
+  taxLines: Array<TaxLine>;
   order: Order;
   customFields?: Maybe<Scalars['JSON']>;
 };

+ 8 - 6
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1817,13 +1817,7 @@ export enum GlobalFlag {
 }
 
 export enum AdjustmentType {
-    TAX = 'TAX',
     PROMOTION = 'PROMOTION',
-    SHIPPING = 'SHIPPING',
-    REFUND = 'REFUND',
-    TAX_REFUND = 'TAX_REFUND',
-    PROMOTION_REFUND = 'PROMOTION_REFUND',
-    SHIPPING_REFUND = 'SHIPPING_REFUND',
 }
 
 export type Adjustment = {
@@ -1833,6 +1827,12 @@ export type Adjustment = {
     amount: Scalars['Int'];
 };
 
+export type TaxLine = {
+    description: Scalars['String'];
+    amount: Scalars['Int'];
+    taxRate: Scalars['Float'];
+};
+
 export type ConfigArg = {
     name: Scalars['String'];
     value: Scalars['String'];
@@ -3200,6 +3200,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     fulfillment?: Maybe<Fulfillment>;
     refundId?: Maybe<Scalars['ID']>;
 };
@@ -3224,6 +3225,7 @@ export type OrderLine = Node & {
     /** The total price of the line including tax */
     linePriceWithTax: Scalars['Int'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     order: Order;
     customFields?: Maybe<Scalars['JSON']>;
 };

+ 18 - 8
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -296,13 +296,7 @@ export enum GlobalFlag {
 }
 
 export enum AdjustmentType {
-    TAX = 'TAX',
     PROMOTION = 'PROMOTION',
-    SHIPPING = 'SHIPPING',
-    REFUND = 'REFUND',
-    TAX_REFUND = 'TAX_REFUND',
-    PROMOTION_REFUND = 'PROMOTION_REFUND',
-    SHIPPING_REFUND = 'SHIPPING_REFUND',
 }
 
 export type Adjustment = {
@@ -312,6 +306,12 @@ export type Adjustment = {
     amount: Scalars['Int'];
 };
 
+export type TaxLine = {
+    description: Scalars['String'];
+    amount: Scalars['Int'];
+    taxRate: Scalars['Float'];
+};
+
 export type ConfigArg = {
     name: Scalars['String'];
     value: Scalars['String'];
@@ -2033,6 +2033,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     fulfillment?: Maybe<Fulfillment>;
     refundId?: Maybe<Scalars['ID']>;
 };
@@ -2057,6 +2058,7 @@ export type OrderLine = Node & {
     /** The total price of the line including tax */
     linePriceWithTax: Scalars['Int'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     order: Order;
     customFields?: Maybe<Scalars['JSON']>;
 };
@@ -2687,11 +2689,11 @@ export type NativeAuthInput = {
 
 export type TestOrderFragmentFragment = Pick<
     Order,
-    'id' | 'code' | 'state' | 'active' | 'total' | 'couponCodes' | 'shipping'
+    'id' | 'code' | 'state' | 'active' | 'totalBeforeTax' | 'total' | 'couponCodes' | 'shipping'
 > & {
     adjustments: Array<Pick<Adjustment, 'adjustmentSource' | 'amount' | 'description' | 'type'>>;
     lines: Array<
-        Pick<OrderLine, 'id' | 'quantity'> & {
+        Pick<OrderLine, 'id' | 'quantity' | 'linePrice' | 'linePriceWithTax'> & {
             productVariant: Pick<ProductVariant, 'id'>;
             adjustments: Array<Pick<Adjustment, 'adjustmentSource' | 'amount' | 'description' | 'type'>>;
         }
@@ -2903,6 +2905,7 @@ export type GetActiveOrderWithPriceDataQuery = {
                 > & {
                     items: Array<Pick<OrderItem, 'id' | 'unitPrice' | 'unitPriceWithTax' | 'taxRate'>>;
                     adjustments: Array<Pick<Adjustment, 'amount' | 'type'>>;
+                    taxLines: Array<Pick<TaxLine, 'amount' | 'taxRate' | 'description'>>;
                 }
             >;
             taxSummary: Array<Pick<OrderTaxSummary, 'taxRate' | 'taxBase' | 'taxTotal'>>;
@@ -3390,6 +3393,13 @@ export namespace GetActiveOrderWithPriceData {
             >['adjustments']
         >[number]
     >;
+    export type TaxLines = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<NonNullable<GetActiveOrderWithPriceDataQuery['activeOrder']>['lines']>[number]
+            >['taxLines']
+        >[number]
+    >;
     export type TaxSummary = NonNullable<
         NonNullable<NonNullable<GetActiveOrderWithPriceDataQuery['activeOrder']>['taxSummary']>[number]
     >;

+ 8 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -6,6 +6,7 @@ export const TEST_ORDER_FRAGMENT = gql`
         code
         state
         active
+        totalBeforeTax
         total
         couponCodes
         adjustments {
@@ -17,6 +18,8 @@ export const TEST_ORDER_FRAGMENT = gql`
         lines {
             id
             quantity
+            linePrice
+            linePriceWithTax
             productVariant {
                 id
             }
@@ -322,6 +325,11 @@ export const GET_ACTIVE_ORDER_WITH_PRICE_DATA = gql`
                     amount
                     type
                 }
+                taxLines {
+                    amount
+                    taxRate
+                    description
+                }
             }
             taxSummary {
                 taxRate

+ 52 - 10
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -16,6 +16,7 @@ import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { orderFixedDiscount } from '../src/config/promotion/actions/order-fixed-discount-action';
 
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import {
@@ -41,6 +42,7 @@ import {
     TestOrderFragment,
     TestOrderFragmentFragment,
     TestOrderWithPaymentsFragment,
+    UpdatedOrder,
     UpdatedOrderFragment,
 } from './graphql/generated-e2e-shop-types';
 import {
@@ -523,6 +525,46 @@ describe('Promotions applied to Orders', () => {
             await deletePromotion(promotion.id);
         });
 
+        it('orderFixedDiscount', async () => {
+            const couponCode = '10_off_order';
+            const promotion = await createPromotion({
+                enabled: true,
+                name: '$10 discount on order',
+                couponCode,
+                conditions: [],
+                actions: [
+                    {
+                        code: orderFixedDiscount.code,
+                        arguments: [{ name: 'discount', value: '1000' }],
+                    },
+                ],
+            });
+            const item60 = getVariantBySlug('item-60');
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: item60.id,
+                quantity: 1,
+            });
+            orderResultGuard.assertSuccess(addItemToOrder);
+            expect(addItemToOrder!.total).toBe(6000);
+            expect(addItemToOrder!.adjustments.length).toBe(0);
+
+            const { applyCouponCode } = await shopClient.query<
+                ApplyCouponCode.Mutation,
+                ApplyCouponCode.Variables
+            >(APPLY_COUPON_CODE, {
+                couponCode,
+            });
+            orderResultGuard.assertSuccess(applyCouponCode);
+            expect(applyCouponCode!.adjustments.length).toBe(1);
+            expect(applyCouponCode!.adjustments[0].description).toBe('$10 discount on order');
+            expect(applyCouponCode!.total).toBe(5000);
+
+            await deletePromotion(promotion.id);
+        });
+
         it('discountOnItemWithFacets', async () => {
             const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
             const saleFacetValue = facets.items[0].values[0];
@@ -558,12 +600,12 @@ describe('Promotions applied to Orders', () => {
                 quantity: 2,
             });
 
-            function getItemSale1Line(lines: TestOrderFragment.Lines[]): TestOrderFragment.Lines {
+            function getItemSale1Line(lines: UpdatedOrder.Lines[]): UpdatedOrder.Lines {
                 return lines.find(l => l.productVariant.id === getVariantBySlug('item-sale-1').id)!;
             }
             orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.adjustments.length).toBe(0);
-            expect(getItemSale1Line(addItemToOrder!.lines).adjustments.length).toBe(2); // 2x tax
+            expect(getItemSale1Line(addItemToOrder!.lines).adjustments.length).toBe(0);
             expect(addItemToOrder!.total).toBe(2640);
 
             const { applyCouponCode } = await shopClient.query<
@@ -575,7 +617,7 @@ describe('Promotions applied to Orders', () => {
             orderResultGuard.assertSuccess(applyCouponCode);
 
             expect(applyCouponCode!.total).toBe(1920);
-            expect(getItemSale1Line(applyCouponCode!.lines).adjustments.length).toBe(4); // 2x tax, 2x promotion
+            expect(getItemSale1Line(applyCouponCode!.lines).adjustments.length).toBe(2); // 2x promotion
 
             const { removeCouponCode } = await shopClient.query<
                 RemoveCouponCode.Mutation,
@@ -584,11 +626,11 @@ describe('Promotions applied to Orders', () => {
                 couponCode,
             });
 
-            expect(getItemSale1Line(removeCouponCode!.lines).adjustments.length).toBe(2); // 2x tax
+            expect(getItemSale1Line(removeCouponCode!.lines).adjustments.length).toBe(0);
             expect(removeCouponCode!.total).toBe(2640);
 
             const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
-            expect(getItemSale1Line(activeOrder!.lines).adjustments.length).toBe(2); // 2x tax
+            expect(getItemSale1Line(activeOrder!.lines).adjustments.length).toBe(0);
             expect(activeOrder!.total).toBe(2640);
 
             await deletePromotion(promotion.id);
@@ -621,7 +663,7 @@ describe('Promotions applied to Orders', () => {
             });
             orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.adjustments.length).toBe(0);
-            expect(addItemToOrder!.lines[0].adjustments.length).toBe(1); // 1x tax
+            expect(addItemToOrder!.lines[0].adjustments.length).toBe(0);
             expect(addItemToOrder!.total).toBe(6000);
 
             const { applyCouponCode } = await shopClient.query<
@@ -633,7 +675,7 @@ describe('Promotions applied to Orders', () => {
             orderResultGuard.assertSuccess(applyCouponCode);
 
             expect(applyCouponCode!.total).toBe(3000);
-            expect(applyCouponCode!.lines[0].adjustments.length).toBe(2); // 1x tax, 1x promotion
+            expect(applyCouponCode!.lines[0].adjustments.length).toBe(1); // 1x promotion
 
             const { removeCouponCode } = await shopClient.query<
                 RemoveCouponCode.Mutation,
@@ -642,7 +684,7 @@ describe('Promotions applied to Orders', () => {
                 couponCode,
             });
 
-            expect(removeCouponCode!.lines[0].adjustments.length).toBe(1); // 1x tax
+            expect(removeCouponCode!.lines[0].adjustments.length).toBe(0);
             expect(removeCouponCode!.total).toBe(6000);
 
             await deletePromotion(promotion.id);
@@ -693,7 +735,7 @@ describe('Promotions applied to Orders', () => {
             });
             orderResultGuard.assertSuccess(apply1);
 
-            expect(apply1?.lines[0].adjustments.length).toBe(2);
+            expect(apply1?.lines[0].adjustments.length).toBe(1); // 1x promotion
             expect(
                 apply1?.lines[0].adjustments.find(a => a.type === AdjustmentType.PROMOTION)?.description,
             ).toBe('item promo');
@@ -708,7 +750,7 @@ describe('Promotions applied to Orders', () => {
             });
             orderResultGuard.assertSuccess(apply2);
 
-            expect(apply2?.lines[0].adjustments.length).toBe(2);
+            expect(apply2?.lines[0].adjustments.length).toBe(1);
             expect(
                 apply2?.lines[0].adjustments.find(a => a.type === AdjustmentType.PROMOTION)?.description,
             ).toBe('item promo');

+ 10 - 6
packages/core/e2e/order-taxes.e2e-spec.ts

@@ -81,13 +81,15 @@ describe('Order taxes', () => {
             expect(activeOrder?.lines[0].items[0].unitPrice).toBe(100);
             expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(120);
             expect(activeOrder?.lines[0].items[0].taxRate).toBe(20);
-            expect(activeOrder?.lines[0].adjustments).toEqual([
+            expect(activeOrder?.lines[0].taxLines).toEqual([
                 {
-                    type: AdjustmentType.TAX,
+                    description: 'Standard Tax Europe',
+                    taxRate: 20,
                     amount: 20,
                 },
                 {
-                    type: AdjustmentType.TAX,
+                    description: 'Standard Tax Europe',
+                    taxRate: 20,
                     amount: 20,
                 },
             ]);
@@ -126,13 +128,15 @@ describe('Order taxes', () => {
             expect(activeOrder?.lines[0].items[0].unitPrice).toBe(83);
             expect(activeOrder?.lines[0].items[0].unitPriceWithTax).toBe(100);
             expect(activeOrder?.lines[0].items[0].taxRate).toBe(20);
-            expect(activeOrder?.lines[0].adjustments).toEqual([
+            expect(activeOrder?.lines[0].taxLines).toEqual([
                 {
-                    type: AdjustmentType.TAX,
+                    description: 'Standard Tax Europe',
+                    taxRate: 20,
                     amount: 17,
                 },
                 {
-                    type: AdjustmentType.TAX,
+                    description: 'Standard Tax Europe',
+                    taxRate: 20,
                     amount: 17,
                 },
             ]);

+ 6 - 6
packages/core/src/api/schema/common/common-types.graphql

@@ -10,13 +10,7 @@ enum GlobalFlag {
 }
 
 enum AdjustmentType {
-    TAX
     PROMOTION
-    SHIPPING
-    REFUND
-    TAX_REFUND
-    PROMOTION_REFUND
-    SHIPPING_REFUND
 }
 
 type Adjustment {
@@ -26,6 +20,12 @@ type Adjustment {
     amount: Int!
 }
 
+type TaxLine {
+    description: String!
+    amount: Int!
+    taxRate: Float!
+}
+
 type ConfigArg {
     name: String!
     value: String!

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

@@ -89,6 +89,7 @@ type OrderItem implements Node {
     unitPriceIncludesTax: Boolean! @deprecated(reason: "`unitPrice` is now always without tax")
     taxRate: Float!
     adjustments: [Adjustment!]!
+    taxLines: [TaxLine!]!
     fulfillment: Fulfillment
     refundId: ID
 }
@@ -112,6 +113,7 @@ type OrderLine implements Node {
     "The total price of the line including tax"
     linePriceWithTax: Int!
     adjustments: [Adjustment!]!
+    taxLines: [TaxLine!]!
     order: Order!
 }
 

+ 19 - 0
packages/core/src/config/promotion/actions/order-fixed-discount-action.ts

@@ -0,0 +1,19 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+
+import { PromotionOrderAction } from '../promotion-action';
+
+export const orderFixedDiscount = new PromotionOrderAction({
+    code: 'order_fixed_discount',
+    args: {
+        discount: {
+            type: 'int',
+            ui: {
+                component: 'currency-form-input',
+            },
+        },
+    },
+    execute(ctx, order, args) {
+        return -args.discount;
+    },
+    description: [{ languageCode: LanguageCode.en, value: 'Discount order by fixed amount' }],
+});

+ 2 - 0
packages/core/src/config/promotion/index.ts

@@ -1,4 +1,5 @@
 import { discountOnItemWithFacets } from './actions/facet-values-discount-action';
+import { orderFixedDiscount } from './actions/order-fixed-discount-action';
 import { orderPercentageDiscount } from './actions/order-percentage-discount-action';
 import { productsPercentageDiscount } from './actions/product-discount-action';
 import { containsProducts } from './conditions/contains-products-condition';
@@ -18,6 +19,7 @@ export * from './conditions/customer-group-condition';
 export * from './utils/facet-value-checker';
 
 export const defaultPromotionActions = [
+    orderFixedDiscount,
     orderPercentageDiscount,
     discountOnItemWithFacets,
     productsPercentageDiscount,

+ 1 - 0
packages/core/src/config/shipping-method/shipping-calculator.ts

@@ -65,6 +65,7 @@ export class ShippingCalculator<T extends ConfigArgs = ConfigArgs> extends Confi
  */
 export interface ShippingCalculationResult {
     /**
+     * @description
      * The shipping price without any taxes.
      */
     price: number;

+ 18 - 33
packages/core/src/entity/order-item/order-item.entity.ts

@@ -1,4 +1,4 @@
-import { Adjustment, AdjustmentType } from '@vendure/common/lib/generated-types';
+import { Adjustment, AdjustmentType, TaxLine } from '@vendure/common/lib/generated-types';
 import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
 import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToOne } from 'typeorm';
 
@@ -34,10 +34,10 @@ export class OrderItem extends VendureEntity {
      */
     unitPriceIncludesTax = false;
 
-    @Column({ type: 'decimal', precision: 5, scale: 2, transformer: new DecimalTransformer() })
-    taxRate: number;
+    @Column('simple-json') adjustments: Adjustment[];
 
-    @Column('simple-json') pendingAdjustments: Adjustment[];
+    @Column('simple-json')
+    taxLines: TaxLine[];
 
     @ManyToMany(type => Fulfillment, fulfillment => fulfillment.orderItems)
     @JoinTable()
@@ -59,31 +59,19 @@ export class OrderItem extends VendureEntity {
         return this.fulfillments?.find(f => f.state !== 'Cancelled');
     }
 
+    /**
+     * @description
+     * The total applicable tax rate, which is the sum of all taxLines on this
+     * OrderItem.
+     */
     @Calculated()
-    get unitPriceWithTax(): number {
-        return Math.round(this.unitPrice * ((100 + this.taxRate) / 100));
+    get taxRate(): number {
+        return this.taxLines.reduce((total, l) => total + l.taxRate, 0);
     }
 
-    /**
-     * Adjustments with promotion values adjusted to include tax.
-     */
     @Calculated()
-    get adjustments(): Adjustment[] {
-        if (!this.pendingAdjustments) {
-            return [];
-        }
-        return this.pendingAdjustments.map(a => {
-            if (a.type === AdjustmentType.PROMOTION) {
-                // Add the tax that would have been payable on the discount so that the numbers add up
-                // for the end-user.
-                const adjustmentWithTax = Math.round(a.amount * ((100 + this.taxRate) / 100));
-                return {
-                    ...a,
-                    amount: adjustmentWithTax,
-                };
-            }
-            return a;
-        });
+    get unitPriceWithTax(): number {
+        return Math.round(this.unitPrice * ((100 + this.taxRate) / 100));
     }
 
     /**
@@ -94,15 +82,14 @@ export class OrderItem extends VendureEntity {
     }
 
     get unitTax(): number {
-        const taxAdjustment = this.adjustments.find(a => a.type === AdjustmentType.TAX);
-        return taxAdjustment ? taxAdjustment.amount : 0;
+        return this.taxLines.reduce((total, l) => total + l.amount, 0);
     }
 
     get promotionAdjustmentsTotal(): number {
-        if (!this.pendingAdjustments) {
+        if (!this.adjustments) {
             return 0;
         }
-        return this.pendingAdjustments
+        return this.adjustments
             .filter(a => a.type === AdjustmentType.PROMOTION)
             .reduce((total, a) => total + a.amount, 0);
     }
@@ -113,11 +100,9 @@ export class OrderItem extends VendureEntity {
 
     clearAdjustments(type?: AdjustmentType) {
         if (!type) {
-            this.pendingAdjustments = [];
+            this.adjustments = [];
         } else {
-            this.pendingAdjustments = this.pendingAdjustments
-                ? this.pendingAdjustments.filter(a => a.type !== type)
-                : [];
+            this.adjustments = this.adjustments ? this.adjustments.filter(a => a.type !== type) : [];
         }
     }
 }

+ 6 - 1
packages/core/src/entity/order-line/order-line.entity.ts

@@ -1,4 +1,4 @@
-import { Adjustment, AdjustmentType } from '@vendure/common/lib/generated-types';
+import { Adjustment, AdjustmentType, TaxLine } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
 import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
 
@@ -74,6 +74,11 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
         );
     }
 
+    @Calculated()
+    get taxLines(): TaxLine[] {
+        return this.activeItems.reduce((taxLines, item) => [...taxLines, ...item.taxLines], [] as TaxLine[]);
+    }
+
     @Calculated()
     get taxRate(): number {
         return this.activeItems.length ? this.activeItems[0].taxRate : 0;

+ 5 - 8
packages/core/src/entity/tax-rate/tax-rate.entity.ts

@@ -1,9 +1,9 @@
-import { Adjustment, AdjustmentType } from '@vendure/common/lib/generated-types';
+import { TaxLine } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
 import { Column, Entity, ManyToOne } from 'typeorm';
 
-import { AdjustmentSource } from '../../common/types/adjustment-source';
 import { idsAreEqual } from '../../common/utils';
+import { VendureEntity } from '../base/base.entity';
 import { CustomerGroup } from '../customer-group/customer-group.entity';
 import { TaxCategory } from '../tax-category/tax-category.entity';
 import { DecimalTransformer } from '../value-transformers';
@@ -20,9 +20,7 @@ import { Zone } from '../zone/zone.entity';
  * @docsCategory entities
  */
 @Entity()
-export class TaxRate extends AdjustmentSource {
-    readonly type = AdjustmentType.TAX;
-
+export class TaxRate extends VendureEntity {
     constructor(input?: DeepPartial<TaxRate>) {
         super(input);
     }
@@ -70,11 +68,10 @@ export class TaxRate extends AdjustmentSource {
         return netPrice + this.taxPayableOn(netPrice);
     }
 
-    apply(price: number): Adjustment {
+    apply(price: number): TaxLine {
         return {
-            type: this.type,
-            adjustmentSource: this.getSourceId(),
             description: this.name,
+            taxRate: this.value,
             amount: this.taxPayableOn(price),
         };
     }

+ 5 - 3
packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -74,6 +74,8 @@ describe('OrderCalculator', () => {
                         () =>
                             new OrderItem({
                                 unitPrice,
+                                taxLines: [],
+                                adjustments: [],
                             }),
                     ),
                 }),
@@ -224,7 +226,7 @@ describe('OrderCalculator', () => {
 
             // increase the quantity to 2, which will take the total over the minimum set by the
             // condition.
-            order.lines[0].items.push(new OrderItem({ unitPrice: 50 }));
+            order.lines[0].items.push(new OrderItem({ unitPrice: 50, taxLines: [], adjustments: [] }));
 
             await orderCalculator.applyPriceAdjustments(ctx, order, [promotion], order.lines[0]);
 
@@ -365,7 +367,7 @@ describe('OrderCalculator', () => {
 
                 // increase the quantity to 3, which will trigger the first promotion and thus
                 // bring the order total below the threshold for the second promotion.
-                order.lines[0].items.push(new OrderItem({ unitPrice: 50 }));
+                order.lines[0].items.push(new OrderItem({ unitPrice: 50, taxLines: [], adjustments: [] }));
 
                 await orderCalculator.applyPriceAdjustments(
                     ctx,
@@ -398,7 +400,7 @@ describe('OrderCalculator', () => {
 
                 // increase the quantity to 3, which will trigger the first promotion and thus
                 // bring the order total below the threshold for the second promotion.
-                order.lines[0].items.push(new OrderItem({ unitPrice: 42 }));
+                order.lines[0].items.push(new OrderItem({ unitPrice: 42, taxLines: [], adjustments: [] }));
 
                 await orderCalculator.applyPriceAdjustments(
                     ctx,

+ 6 - 11
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -107,14 +107,9 @@ export class OrderCalculator {
         activeZone: Zone,
         getTaxRate: (taxCategory: TaxCategory) => TaxRate,
     ) {
-        line.clearAdjustments(AdjustmentType.TAX);
-
         const applicableTaxRate = getTaxRate(line.taxCategory);
         for (const item of line.activeItems) {
-            item.taxRate = applicableTaxRate.value;
-            item.pendingAdjustments = item.pendingAdjustments.concat(
-                applicableTaxRate.apply(item.unitPriceWithPromotions),
-            );
+            item.taxLines = [applicableTaxRate.apply(item.unitPriceWithPromotions)];
         }
     }
 
@@ -170,8 +165,8 @@ export class OrderCalculator {
             const applicablePromotions = await filterAsync(promotions, p => p.test(ctx, order));
 
             const lineHasExistingPromotions =
-                line.items[0].pendingAdjustments &&
-                !!line.items[0].pendingAdjustments.find(a => a.type === AdjustmentType.PROMOTION);
+                line.items[0].adjustments &&
+                !!line.items[0].adjustments.find(a => a.type === AdjustmentType.PROMOTION);
             const forceUpdateItems = this.orderLineHasInapplicablePromotions(applicablePromotions, line);
 
             if (forceUpdateItems || lineHasExistingPromotions) {
@@ -196,7 +191,7 @@ export class OrderCalculator {
                             orderLine: line,
                         });
                         if (adjustment) {
-                            item.pendingAdjustments = item.pendingAdjustments.concat(adjustment);
+                            item.adjustments = item.adjustments.concat(adjustment);
                             priceAdjusted = true;
                             updatedOrderItems.add(item);
                         }
@@ -208,8 +203,8 @@ export class OrderCalculator {
                 }
             }
             const lineNoLongerHasPromotions =
-                !line.items[0].pendingAdjustments ||
-                !line.items[0].pendingAdjustments.find(a => a.type === AdjustmentType.PROMOTION);
+                !line.items[0].adjustments ||
+                !line.items[0].adjustments.find(a => a.type === AdjustmentType.PROMOTION);
             if (lineHasExistingPromotions && lineNoLongerHasPromotions) {
                 line.items.forEach(i => updatedOrderItems.add(i));
             }

+ 2 - 2
packages/core/src/service/services/order-testing.service.ts

@@ -120,8 +120,8 @@ export class OrderTestingService {
             for (let i = 0; i < line.quantity; i++) {
                 const orderItem = new OrderItem({
                     unitPrice,
-                    taxRate: taxRate.value,
-                    pendingAdjustments: [],
+                    adjustments: [],
+                    taxLines: [],
                 });
                 orderLine.items.push(orderItem);
             }

+ 2 - 2
packages/core/src/service/services/order.service.ts

@@ -396,8 +396,8 @@ export class OrderService {
                     const orderItem = await this.connection.getRepository(ctx, OrderItem).save(
                         new OrderItem({
                             unitPrice,
-                            pendingAdjustments: [],
-                            taxRate: taxRate.value,
+                            adjustments: [],
+                            taxLines: [],
                         }),
                     );
                     orderLine.items.push(orderItem);

+ 8 - 6
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1817,13 +1817,7 @@ export enum GlobalFlag {
 }
 
 export enum AdjustmentType {
-    TAX = 'TAX',
     PROMOTION = 'PROMOTION',
-    SHIPPING = 'SHIPPING',
-    REFUND = 'REFUND',
-    TAX_REFUND = 'TAX_REFUND',
-    PROMOTION_REFUND = 'PROMOTION_REFUND',
-    SHIPPING_REFUND = 'SHIPPING_REFUND',
 }
 
 export type Adjustment = {
@@ -1833,6 +1827,12 @@ export type Adjustment = {
     amount: Scalars['Int'];
 };
 
+export type TaxLine = {
+    description: Scalars['String'];
+    amount: Scalars['Int'];
+    taxRate: Scalars['Float'];
+};
+
 export type ConfigArg = {
     name: Scalars['String'];
     value: Scalars['String'];
@@ -3200,6 +3200,7 @@ export type OrderItem = Node & {
     unitPriceIncludesTax: Scalars['Boolean'];
     taxRate: Scalars['Float'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     fulfillment?: Maybe<Fulfillment>;
     refundId?: Maybe<Scalars['ID']>;
 };
@@ -3224,6 +3225,7 @@ export type OrderLine = Node & {
     /** The total price of the line including tax */
     linePriceWithTax: Scalars['Int'];
     adjustments: Array<Adjustment>;
+    taxLines: Array<TaxLine>;
     order: Order;
     customFields?: Maybe<Scalars['JSON']>;
 };

+ 4 - 4
packages/email-plugin/src/mock-events.ts

@@ -45,8 +45,8 @@ export const mockOrderStateTransitionEvent = new OrderStateTransitionEvent(
                         id: '6',
                         unitPrice: 14374,
                         unitPriceIncludesTax: true,
-                        taxRate: 20,
-                        pendingAdjustments: [],
+                        adjustments: [],
+                        taxLines: [],
                     }),
                 ],
             }),
@@ -65,8 +65,8 @@ export const mockOrderStateTransitionEvent = new OrderStateTransitionEvent(
                         id: '7',
                         unitPrice: 3799,
                         unitPriceIncludesTax: true,
-                        taxRate: 20,
-                        pendingAdjustments: [],
+                        adjustments: [],
+                        taxLines: [],
                     }),
                 ],
             }),

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-admin.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-shop.json


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است