Răsfoiți Sursa

feat(server): Apply tax adjustments to OrderItems

Relates to #31
Michael Bromley 7 ani în urmă
părinte
comite
80b965892d

+ 3 - 2
server/src/api/resolvers/order.resolver.ts

@@ -46,8 +46,9 @@ export class OrderResolver {
     @Allow(Permission.Owner)
     async activeOrder(@Ctx() ctx: RequestContext): Promise<Order | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
-            if (ctx.session && ctx.session.activeOrder && ctx.session.activeOrder.id) {
-                const order = await this.orderService.findOne(ctx, ctx.session.activeOrder.id);
+            const sessionOrder = await this.getOrderFromContext(ctx);
+            if (sessionOrder) {
+                const order = await this.orderService.findOne(ctx, sessionOrder.id);
                 return order;
             } else {
                 return;

+ 5 - 1
server/src/config/adjustment/adjustment-types.ts

@@ -9,15 +9,19 @@ export type AdjustmentActionResult = {
     orderItemId?: ID;
     amount: number;
 };
-export type AdjustmentActionCalculation = (
+export type AdjustmentActionCalculation<Context = any> = (
     order: Order,
     args: { [argName: string]: any },
+    context: Context,
 ) => AdjustmentActionResult[];
 
 export interface AdjustmentActionDefinition extends AdjustmentOperation {
     args: AdjustmentActionArg[];
     calculate: AdjustmentActionCalculation;
 }
+export interface TaxActionDefinition extends AdjustmentActionDefinition {
+    calculate: AdjustmentActionCalculation<{ taxCategoryId: ID }>;
+}
 
 export type AdjustmentConditionArgType = 'int' | 'money' | 'string' | 'datetime';
 export type AdjustmentConditionArg = { name: string; type: AdjustmentConditionArgType };

+ 12 - 8
server/src/config/adjustment/required-adjustment-actions.ts

@@ -1,16 +1,20 @@
 import { AdjustmentType } from 'shared/generated-types';
 
-import { AdjustmentActionDefinition } from './adjustment-types';
+import { idsAreEqual } from '../../common/utils';
 
-export const taxAction: AdjustmentActionDefinition = {
+import { AdjustmentActionDefinition, TaxActionDefinition } from './adjustment-types';
+
+export const taxAction: TaxActionDefinition = {
     type: AdjustmentType.TAX,
     code: 'tax_action',
-    args: [{ name: 'discount', type: 'percentage' }],
-    calculate(order, args) {
-        return order.items.map(item => ({
-            orderItemId: item.id,
-            amount: -(item.totalPrice * args.discount) / 100,
-        }));
+    args: [{ name: 'taxRate', type: 'percentage' }],
+    calculate(order, args, context) {
+        return order.items
+            .filter(item => idsAreEqual(item.taxCategoryId, context.taxCategoryId))
+            .map(item => ({
+                orderItemId: item.id,
+                amount: (item.totalPrice * args.taxRate) / 100,
+            }));
     },
     description: 'Apply tax of { discount }%',
 };

+ 31 - 0
server/src/entity/adjustment-source/adjustment-source.entity.ts

@@ -3,6 +3,8 @@ import { DeepPartial, ID } from 'shared/shared-types';
 import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
 
 import { ChannelAware } from '../../common/types/common-types';
+import { taxAction } from '../../config/adjustment/required-adjustment-actions';
+import { taxCondition } from '../../config/adjustment/required-adjustment-conditions';
 import { I18nError } from '../../i18n/i18n-error';
 import { VendureEntity } from '../base/base.entity';
 import { Channel } from '../channel/channel.entity';
@@ -36,6 +38,35 @@ export class AdjustmentSource extends VendureEntity implements ChannelAware {
         }
         return Number(this.actions[0].args[0].value);
     }
+
+    /**
+     * Returns a new AdjustmentSource configured as a tax category.
+     */
+    static createTaxCategory(taxRate: number, name: string, id?: ID): AdjustmentSource {
+        return new AdjustmentSource({
+            id,
+            name,
+            type: AdjustmentType.TAX,
+            conditions: [
+                {
+                    code: taxCondition.code,
+                    args: [],
+                },
+            ],
+            actions: [
+                {
+                    code: taxAction.code,
+                    args: [
+                        {
+                            type: 'percentage',
+                            name: 'taxRate',
+                            value: taxRate.toString(),
+                        },
+                    ],
+                },
+            ],
+        });
+    }
 }
 
 /**

+ 11 - 1
server/src/entity/order-item/order-item.entity.ts

@@ -1,4 +1,4 @@
-import { DeepPartial } from 'shared/shared-types';
+import { DeepPartial, ID } from 'shared/shared-types';
 import { Column, Entity, ManyToOne } from 'typeorm';
 
 import { Adjustment } from '../adjustment-source/adjustment-source.entity';
@@ -16,9 +16,19 @@ export class OrderItem extends VendureEntity {
     @ManyToOne(type => ProductVariant)
     productVariant: ProductVariant;
 
+    @Column('varchar') taxCategoryId: ID;
+
     @ManyToOne(type => Asset)
     featuredAsset: Asset;
 
+    /**
+     * Corresponds to the priceBeforeTax value of the associated ProductVariant.
+     */
+    @Column() unitPriceBeforeTax: number;
+
+    /**
+     * Corresponds to the price value of the associated ProductVariant.
+     */
     @Column() unitPrice: number;
 
     @Column() quantity: number;

+ 16 - 55
server/src/service/helpers/apply-adjustments.spec.ts

@@ -4,6 +4,8 @@ import {
     AdjustmentActionDefinition,
     AdjustmentConditionDefinition,
 } from '../../config/adjustment/adjustment-types';
+import { taxAction } from '../../config/adjustment/required-adjustment-actions';
+import { taxCondition } from '../../config/adjustment/required-adjustment-conditions';
 import { AdjustmentSource } from '../../entity/adjustment-source/adjustment-source.entity';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { Order } from '../../entity/order/order.entity';
@@ -51,29 +53,6 @@ describe('applyAdjustments()', () => {
         },
     };
 
-    const alwaysTrueCondition: AdjustmentConditionDefinition = {
-        code: 'always_true',
-        description: 'Always returns true',
-        args: [],
-        type: AdjustmentType.TAX,
-        predicate: (order, args) => {
-            return true;
-        },
-    };
-
-    const standardTaxAction: AdjustmentActionDefinition = {
-        code: 'standard_tax',
-        description: 'Adds standard sales tax of { percentage }%',
-        args: [{ name: 'percentage', type: 'percentage' }],
-        type: AdjustmentType.TAX,
-        calculate: (order, args) => {
-            return order.items.map(item => ({
-                orderItemId: item.id,
-                amount: item.totalPrice * (args.percentage / 100),
-            }));
-        },
-    };
-
     const promoSource1 = new AdjustmentSource({
         id: 'ps1',
         name: 'Promo source 1',
@@ -104,32 +83,11 @@ describe('applyAdjustments()', () => {
         ],
     });
 
-    const standardTaxSource = new AdjustmentSource({
-        id: 'ts1',
-        name: 'Tax source',
-        type: AdjustmentType.TAX,
-        conditions: [
-            {
-                code: alwaysTrueCondition.code,
-                args: [],
-            },
-        ],
-        actions: [
-            {
-                code: standardTaxAction.code,
-                args: [
-                    {
-                        type: 'percentage',
-                        name: 'percentage',
-                        value: '20',
-                    },
-                ],
-            },
-        ],
-    });
+    const standardTaxSource = AdjustmentSource.createTaxCategory(20, 'Standard Tax', 'ts1');
+    const zeroTaxSource = AdjustmentSource.createTaxCategory(0, 'Zero Tax 2', 'ts2');
 
-    const conditions = [minOrderTotalCondition, alwaysTrueCondition];
-    const actions = [orderDiscountAction, standardTaxAction];
+    const conditions = [minOrderTotalCondition, taxCondition];
+    const actions = [orderDiscountAction, taxAction];
 
     it('applies a promo source to an order', () => {
         const order = new Order({
@@ -167,18 +125,20 @@ describe('applyAdjustments()', () => {
                     unitPrice: 300,
                     quantity: 2,
                     totalPriceBeforeAdjustment: 600,
+                    taxCategoryId: standardTaxSource.id,
                 }),
                 new OrderItem({
                     id: 'oi2',
                     unitPrice: 450,
                     quantity: 1,
                     totalPriceBeforeAdjustment: 450,
+                    taxCategoryId: zeroTaxSource.id,
                 }),
             ],
             totalPriceBeforeAdjustment: 1050,
         });
 
-        applyAdjustments(order, [standardTaxSource], conditions, actions);
+        applyAdjustments(order, [standardTaxSource, zeroTaxSource], conditions, actions);
 
         expect(order.adjustments).toEqual([]);
         expect(order.items[0].adjustments).toEqual([
@@ -191,14 +151,14 @@ describe('applyAdjustments()', () => {
         expect(order.items[0].totalPrice).toBe(720);
         expect(order.items[1].adjustments).toEqual([
             {
-                adjustmentSourceId: standardTaxSource.id,
-                description: standardTaxSource.name,
-                amount: 90,
+                adjustmentSourceId: zeroTaxSource.id,
+                description: zeroTaxSource.name,
+                amount: 0,
             },
         ]);
-        expect(order.items[1].totalPrice).toBe(540);
+        expect(order.items[1].totalPrice).toBe(450);
 
-        expect(order.totalPrice).toBe(1260);
+        expect(order.totalPrice).toBe(1170);
     });
 
     it('evaluates promo conditions on items after tax is applied', () => {
@@ -210,12 +170,13 @@ describe('applyAdjustments()', () => {
                     unitPrice: 240,
                     quantity: 2,
                     totalPriceBeforeAdjustment: 480,
+                    taxCategoryId: standardTaxSource.id,
                 }),
             ],
             totalPriceBeforeAdjustment: 480,
         });
 
-        applyAdjustments(order, [promoSource1, standardTaxSource], conditions, actions);
+        applyAdjustments(order, [promoSource1, standardTaxSource, zeroTaxSource], conditions, actions);
 
         expect(order.items[0].adjustments).toEqual([
             {

+ 9 - 7
server/src/service/helpers/apply-adjustments.ts

@@ -6,8 +6,7 @@ import {
     AdjustmentActionResult,
     AdjustmentConditionDefinition,
 } from '../../config/adjustment/adjustment-types';
-import { AdjustmentSource } from '../../entity/adjustment-source/adjustment-source.entity';
-import { Adjustment } from '../../entity/adjustment-source/adjustment-source.entity';
+import { Adjustment, AdjustmentSource } from '../../entity/adjustment-source/adjustment-source.entity';
 import { Order } from '../../entity/order/order.entity';
 
 /**
@@ -114,11 +113,14 @@ function applyActionCalculations(
         if (!actionConfig) {
             continue;
         }
-        const actionResults = actionConfig.calculate(order, argsArrayToHash(action.args)).map(result => {
-            // Do not allow fractions of pennies.
-            result.amount = Math.round(result.amount);
-            return result;
-        });
+        const context = source.type === AdjustmentType.TAX ? { taxCategoryId: source.id } : {};
+        const actionResults = actionConfig
+            .calculate(order, argsArrayToHash(action.args), context)
+            .map(result => {
+                // Do not allow fractions of pennies.
+                result.amount = Math.round(result.amount);
+                return result;
+            });
         results = [...results, ...actionResults];
     }
     return results;

+ 4 - 2
server/src/service/providers/order.service.ts

@@ -74,10 +74,12 @@ export class OrderService {
         const orderItem = new OrderItem({
             quantity,
             productVariant,
+            taxCategoryId: productVariant.taxCategory.id,
             featuredAsset: productVariant.product.featuredAsset,
             unitPrice: productVariant.price,
-            totalPriceBeforeAdjustment: productVariant.price * quantity,
-            totalPrice: productVariant.price * quantity,
+            unitPriceBeforeTax: productVariant.priceBeforeTax,
+            totalPriceBeforeAdjustment: productVariant.priceBeforeTax * quantity,
+            totalPrice: productVariant.priceBeforeTax * quantity,
             adjustments: [],
         });
         const newOrderItem = await this.connection.getRepository(OrderItem).save(orderItem);