|
@@ -1,30 +1,29 @@
|
|
|
import { InjectConnection } from '@nestjs/typeorm';
|
|
import { InjectConnection } from '@nestjs/typeorm';
|
|
|
-import { Adjustment, AdjustmentType } from 'shared/generated-types';
|
|
|
|
|
|
|
+import { AdjustmentType } from 'shared/generated-types';
|
|
|
import { ID, PaginatedList } from 'shared/shared-types';
|
|
import { ID, PaginatedList } from 'shared/shared-types';
|
|
|
import { Connection } from 'typeorm';
|
|
import { Connection } from 'typeorm';
|
|
|
|
|
|
|
|
import { RequestContext } from '../../api/common/request-context';
|
|
import { RequestContext } from '../../api/common/request-context';
|
|
|
import { generatePublicId } from '../../common/generate-public-id';
|
|
import { generatePublicId } from '../../common/generate-public-id';
|
|
|
import { ListQueryOptions } from '../../common/types/common-types';
|
|
import { ListQueryOptions } from '../../common/types/common-types';
|
|
|
-import { assertFound, idsAreEqual } from '../../common/utils';
|
|
|
|
|
|
|
+import { idsAreEqual } from '../../common/utils';
|
|
|
import { OrderItem } from '../../entity/order-item/order-item.entity';
|
|
import { OrderItem } from '../../entity/order-item/order-item.entity';
|
|
|
import { OrderLine } from '../../entity/order-line/order-line.entity';
|
|
import { OrderLine } from '../../entity/order-line/order-line.entity';
|
|
|
import { Order } from '../../entity/order/order.entity';
|
|
import { Order } from '../../entity/order/order.entity';
|
|
|
import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
|
|
import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
|
|
|
import { Promotion } from '../../entity/promotion/promotion.entity';
|
|
import { Promotion } from '../../entity/promotion/promotion.entity';
|
|
|
import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
|
|
import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
|
|
|
|
|
+import { Zone } from '../../entity/zone/zone.entity';
|
|
|
import { I18nError } from '../../i18n/i18n-error';
|
|
import { I18nError } from '../../i18n/i18n-error';
|
|
|
import { buildListQuery } from '../helpers/build-list-query';
|
|
import { buildListQuery } from '../helpers/build-list-query';
|
|
|
import { translateDeep } from '../helpers/translate-entity';
|
|
import { translateDeep } from '../helpers/translate-entity';
|
|
|
|
|
|
|
|
-import { AdjustmentApplicatorService } from './adjustment-applicator.service';
|
|
|
|
|
import { ProductVariantService } from './product-variant.service';
|
|
import { ProductVariantService } from './product-variant.service';
|
|
|
|
|
|
|
|
export class OrderService {
|
|
export class OrderService {
|
|
|
constructor(
|
|
constructor(
|
|
|
@InjectConnection() private connection: Connection,
|
|
@InjectConnection() private connection: Connection,
|
|
|
private productVariantService: ProductVariantService,
|
|
private productVariantService: ProductVariantService,
|
|
|
- private adjustmentApplicatorService: AdjustmentApplicatorService,
|
|
|
|
|
) {}
|
|
) {}
|
|
|
|
|
|
|
|
findAll(ctx: RequestContext, options?: ListQueryOptions<Order>): Promise<PaginatedList<Order>> {
|
|
findAll(ctx: RequestContext, options?: ListQueryOptions<Order>): Promise<PaginatedList<Order>> {
|
|
@@ -61,6 +60,7 @@ export class OrderService {
|
|
|
code: generatePublicId(),
|
|
code: generatePublicId(),
|
|
|
lines: [],
|
|
lines: [],
|
|
|
totalPrice: 0,
|
|
totalPrice: 0,
|
|
|
|
|
+ totalPriceBeforeTax: 0,
|
|
|
});
|
|
});
|
|
|
return this.connection.getRepository(Order).save(newOrder);
|
|
return this.connection.getRepository(Order).save(newOrder);
|
|
|
}
|
|
}
|
|
@@ -116,14 +116,14 @@ export class OrderService {
|
|
|
orderLine.items = orderLine.items.slice(0, quantity);
|
|
orderLine.items = orderLine.items.slice(0, quantity);
|
|
|
}
|
|
}
|
|
|
await this.connection.getRepository(OrderLine).save(orderLine);
|
|
await this.connection.getRepository(OrderLine).save(orderLine);
|
|
|
- return this.calculateOrderTotals(ctx, order);
|
|
|
|
|
|
|
+ return this.applyAdjustments(ctx, order);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async removeItemFromOrder(ctx: RequestContext, orderId: ID, orderLineId: ID): Promise<Order> {
|
|
async removeItemFromOrder(ctx: RequestContext, orderId: ID, orderLineId: ID): Promise<Order> {
|
|
|
const order = await this.getOrderOrThrow(ctx, orderId);
|
|
const order = await this.getOrderOrThrow(ctx, orderId);
|
|
|
const orderLine = this.getOrderLineOrThrow(order, orderLineId);
|
|
const orderLine = this.getOrderLineOrThrow(order, orderLineId);
|
|
|
order.lines = order.lines.filter(line => !idsAreEqual(line.id, orderLineId));
|
|
order.lines = order.lines.filter(line => !idsAreEqual(line.id, orderLineId));
|
|
|
- const updatedOrder = await this.calculateOrderTotals(ctx, order);
|
|
|
|
|
|
|
+ const updatedOrder = await this.applyAdjustments(ctx, order);
|
|
|
await this.connection.getRepository(OrderLine).remove(orderLine);
|
|
await this.connection.getRepository(OrderLine).remove(orderLine);
|
|
|
return updatedOrder;
|
|
return updatedOrder;
|
|
|
}
|
|
}
|
|
@@ -167,7 +167,8 @@ export class OrderService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private async calculateOrderTotals(ctx: RequestContext, order: Order): Promise<Order> {
|
|
|
|
|
|
|
+ // TODO: Refactor the mail calculation logic out into a more testable service.
|
|
|
|
|
+ private async applyAdjustments(ctx: RequestContext, order: Order): Promise<Order> {
|
|
|
const activeZone = ctx.channel.defaultTaxZone;
|
|
const activeZone = ctx.channel.defaultTaxZone;
|
|
|
const taxRates = await this.connection.getRepository(TaxRate).find({
|
|
const taxRates = await this.connection.getRepository(TaxRate).find({
|
|
|
where: {
|
|
where: {
|
|
@@ -178,30 +179,74 @@ export class OrderService {
|
|
|
});
|
|
});
|
|
|
const promotions = await this.connection.getRepository(Promotion).find({ where: { enabled: true } });
|
|
const promotions = await this.connection.getRepository(Promotion).find({ where: { enabled: true } });
|
|
|
|
|
|
|
|
|
|
+ order.clearAdjustments();
|
|
|
|
|
+ // First apply taxes to the non-discounted prices
|
|
|
|
|
+ this.applyTaxes(order, taxRates, activeZone);
|
|
|
|
|
+ // Then test and apply promotions
|
|
|
|
|
+ this.applyPromotions(order, promotions);
|
|
|
|
|
+ // Finally, re-calculate taxes because the promotions may have
|
|
|
|
|
+ // altered the unit prices, which in turn will alter the tax payable.
|
|
|
|
|
+ this.applyTaxes(order, taxRates, activeZone);
|
|
|
|
|
+
|
|
|
|
|
+ await this.connection.getRepository(Order).save(order);
|
|
|
|
|
+ await this.connection.getRepository(OrderItem).save(order.getOrderItems());
|
|
|
|
|
+ return order;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Applies the correct TaxRate to each OrderItem in the order.
|
|
|
|
|
+ */
|
|
|
|
|
+ private applyTaxes(order: Order, taxRates: TaxRate[], activeZone: Zone) {
|
|
|
for (const line of order.lines) {
|
|
for (const line of order.lines) {
|
|
|
const applicableTaxRate = taxRates.find(taxRate => taxRate.test(activeZone, line.taxCategory));
|
|
const applicableTaxRate = taxRates.find(taxRate => taxRate.test(activeZone, line.taxCategory));
|
|
|
|
|
|
|
|
|
|
+ line.clearAdjustments(AdjustmentType.TAX);
|
|
|
|
|
+
|
|
|
for (const item of line.items) {
|
|
for (const item of line.items) {
|
|
|
if (applicableTaxRate) {
|
|
if (applicableTaxRate) {
|
|
|
- item.pendingAdjustments = [];
|
|
|
|
|
item.pendingAdjustments = item.pendingAdjustments.concat(
|
|
item.pendingAdjustments = item.pendingAdjustments.concat(
|
|
|
- applicableTaxRate.apply(line.unitPrice),
|
|
|
|
|
|
|
+ applicableTaxRate.apply(line.unitPriceWithPromotions),
|
|
|
);
|
|
);
|
|
|
- await this.connection.getRepository(OrderItem).save(item);
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ this.calculateOrderTotals(order);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Applies any eligible promotions to each OrderItem in the order.
|
|
|
|
|
+ */
|
|
|
|
|
+ private applyPromotions(order: Order, promotions: Promotion[]) {
|
|
|
|
|
+ for (const line of order.lines) {
|
|
|
|
|
+ const applicablePromotions = promotions.filter(p => p.test(order));
|
|
|
|
|
|
|
|
- const totalPrice = order.lines.reduce((total, line) => total + line.totalPrice, 0);
|
|
|
|
|
- const totalTax = order.lines
|
|
|
|
|
- .reduce((adjustments, line) => [...adjustments, ...line.adjustments], [] as Adjustment[])
|
|
|
|
|
- .filter(a => a.type === AdjustmentType.TAX)
|
|
|
|
|
- .reduce((total, a) => total + a.amount, 0);
|
|
|
|
|
|
|
+ line.clearAdjustments(AdjustmentType.PROMOTION);
|
|
|
|
|
+
|
|
|
|
|
+ for (const item of line.items) {
|
|
|
|
|
+ if (applicablePromotions) {
|
|
|
|
|
+ for (const promotion of applicablePromotions) {
|
|
|
|
|
+ const adjustment = promotion.apply(item, line);
|
|
|
|
|
+ if (adjustment) {
|
|
|
|
|
+ item.pendingAdjustments = item.pendingAdjustments.concat(adjustment);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ this.calculateOrderTotals(order);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private calculateOrderTotals(order: Order) {
|
|
|
|
|
+ let totalPrice = 0;
|
|
|
|
|
+ let totalTax = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (const line of order.lines) {
|
|
|
|
|
+ totalPrice += line.totalPrice;
|
|
|
|
|
+ totalTax += line.unitTax * line.quantity;
|
|
|
|
|
+ }
|
|
|
const totalPriceBeforeTax = totalPrice - totalTax;
|
|
const totalPriceBeforeTax = totalPrice - totalTax;
|
|
|
|
|
|
|
|
order.totalPriceBeforeTax = totalPriceBeforeTax;
|
|
order.totalPriceBeforeTax = totalPriceBeforeTax;
|
|
|
order.totalPrice = totalPrice;
|
|
order.totalPrice = totalPrice;
|
|
|
- await this.connection.getRepository(Order).save(order);
|
|
|
|
|
- return order;
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|