Browse Source

refactor: Rename AdjustmentSource -> Promotion, refactor OrderItems

Now there is an extra layer - Order -> OrderLine -> OrderItem which will allow better control over unit-level adjustments.
Michael Bromley 7 years ago
parent
commit
0e50a0b312
52 changed files with 873 additions and 1036 deletions
  1. 0 2
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts
  2. 1 1
      admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.ts
  3. 0 85
      admin-ui/src/app/data/definitions/adjustment-source-definitions.ts
  4. 83 0
      admin-ui/src/app/data/definitions/promotion-definitions.ts
  5. 0 89
      admin-ui/src/app/data/providers/adjustment-source-data.service.ts
  6. 1 1
      admin-ui/src/app/data/providers/data.service.mock.ts
  7. 3 3
      admin-ui/src/app/data/providers/data.service.ts
  8. 63 0
      admin-ui/src/app/data/providers/promotion-data.service.ts
  9. 17 21
      admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts
  10. 5 5
      admin-ui/src/app/marketing/components/promotion-list/promotion-list.component.ts
  11. 2 2
      admin-ui/src/app/marketing/marketing.routes.ts
  12. 4 5
      admin-ui/src/app/marketing/providers/routing/promotion-resolver.ts
  13. 0 5
      admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.html
  14. 4 19
      admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.ts
  15. 0 2
      admin-ui/src/app/settings/components/tax-category-list/tax-category-list.component.html
  16. 1 1
      admin-ui/src/app/settings/providers/routing/country-resolver.ts
  17. 4 10
      admin-ui/src/app/settings/providers/routing/tax-category-resolver.ts
  18. 3 3
      admin-ui/src/app/settings/settings.routes.ts
  19. 0 0
      schema.json
  20. 7 7
      server/e2e/adjustment-source.e2e-spec.ts
  21. 3 3
      server/e2e/order.e2e-spec.ts
  22. 0 4
      server/mock-data/mock-data.service.ts
  23. 2 2
      server/src/api/api.module.ts
  24. 0 73
      server/src/api/resolvers/adjustment-source.resolver.ts
  25. 59 0
      server/src/api/resolvers/promotion.resolver.ts
  26. 0 41
      server/src/api/types/adjustment-source.api.graphql
  27. 41 0
      server/src/api/types/promotion.api.graphql
  28. 18 0
      server/src/common/calculated-decorator.ts
  29. 1 1
      server/src/common/types/adjustment-source.ts
  30. 6 0
      server/src/common/types/common-types.graphql
  31. 1 5
      server/src/config/adjustment/default-adjustment-actions.ts
  32. 1 6
      server/src/config/adjustment/default-adjustment-conditions.ts
  33. 1 4
      server/src/config/adjustment/required-adjustment-actions.ts
  34. 0 3
      server/src/config/adjustment/required-adjustment-conditions.ts
  35. 0 70
      server/src/entity/adjustment-source/adjustment-source.entity.ts
  36. 2 1
      server/src/entity/entities.ts
  37. 6 31
      server/src/entity/order-item/order-item.entity.ts
  38. 0 7
      server/src/entity/order-item/order-item.graphql
  39. 43 0
      server/src/entity/order-line/order-line.entity.ts
  40. 12 0
      server/src/entity/order-line/order-line.graphql
  41. 7 7
      server/src/entity/order/order.entity.ts
  42. 2 2
      server/src/entity/order/order.graphql
  43. 3 17
      server/src/entity/promotion/promotion.graphql
  44. 32 0
      server/src/entity/subscribers.ts
  45. 17 17
      server/src/service/helpers/apply-adjustments.spec-off.ts
  46. 3 3
      server/src/service/helpers/apply-adjustments.ts
  47. 7 7
      server/src/service/providers/adjustment-applicator.service.ts
  48. 36 35
      server/src/service/providers/order.service.ts
  49. 3 6
      server/src/service/providers/product-variant.service.ts
  50. 24 45
      server/src/service/providers/promotion.service.ts
  51. 2 2
      server/src/service/service.module.ts
  52. 343 383
      shared/generated-types.ts

+ 0 - 2
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -4,10 +4,8 @@ import { ActivatedRoute, Router } from '@angular/router';
 import { combineLatest, EMPTY, forkJoin, Observable } from 'rxjs';
 import { map, mergeMap, take } from 'rxjs/operators';
 import {
-    AdjustmentSource,
     CreateProductInput,
     LanguageCode,
-    ProductVariant,
     ProductWithVariants,
     TaxCategory,
     UpdateProductInput,

+ 1 - 1
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.ts

@@ -1,6 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { FormArray, FormControl } from '@angular/forms';
-import { AdjustmentSource, ProductWithVariants, TaxCategory } from 'shared/generated-types';
+import { ProductWithVariants, TaxCategory } from 'shared/generated-types';
 
 @Component({
     selector: 'vdr-product-variants-list',

+ 0 - 85
admin-ui/src/app/data/definitions/adjustment-source-definitions.ts

@@ -1,85 +0,0 @@
-import gql from 'graphql-tag';
-
-export const ADJUSTMENT_OPERATION_FRAGMENT = gql`
-    fragment AdjustmentOperation on AdjustmentOperation {
-        args {
-            name
-            type
-            value
-        }
-        code
-        description
-        type
-    }
-`;
-
-export const ADJUSTMENT_SOURCE_FRAGMENT = gql`
-    fragment AdjustmentSource on AdjustmentSource {
-        id
-        createdAt
-        updatedAt
-        name
-        type
-        enabled
-        conditions {
-            ...AdjustmentOperation
-        }
-        actions {
-            ...AdjustmentOperation
-        }
-    }
-    ${ADJUSTMENT_OPERATION_FRAGMENT}
-`;
-
-export const GET_ADJUSTMENT_SOURCE_LIST = gql`
-    query GetAdjustmentSourceList($type: AdjustmentType!, $options: AdjustmentSourceListOptions) {
-        adjustmentSources(type: $type, options: $options) {
-            items {
-                ...AdjustmentSource
-            }
-            totalItems
-        }
-    }
-    ${ADJUSTMENT_SOURCE_FRAGMENT}
-`;
-
-export const GET_ADJUSTMENT_SOURCE = gql`
-    query GetAdjustmentSource($id: ID!) {
-        adjustmentSource(id: $id) {
-            ...AdjustmentSource
-        }
-    }
-    ${ADJUSTMENT_SOURCE_FRAGMENT}
-`;
-
-export const GET_ADJUSTMENT_OPERATIONS = gql`
-    query GetAdjustmentOperations($type: AdjustmentType!) {
-        adjustmentOperations(type: $type) {
-            actions {
-                ...AdjustmentOperation
-            }
-            conditions {
-                ...AdjustmentOperation
-            }
-        }
-    }
-    ${ADJUSTMENT_OPERATION_FRAGMENT}
-`;
-
-export const CREATE_ADJUSTMENT_SOURCE = gql`
-    mutation CreateAdjustmentSource($input: CreateAdjustmentSourceInput!) {
-        createAdjustmentSource(input: $input) {
-            ...AdjustmentSource
-        }
-    }
-    ${ADJUSTMENT_SOURCE_FRAGMENT}
-`;
-
-export const UPDATE_ADJUSTMENT_SOURCE = gql`
-    mutation UpdateAdjustmentSource($input: UpdateAdjustmentSourceInput!) {
-        updateAdjustmentSource(input: $input) {
-            ...AdjustmentSource
-        }
-    }
-    ${ADJUSTMENT_SOURCE_FRAGMENT}
-`;

+ 83 - 0
admin-ui/src/app/data/definitions/promotion-definitions.ts

@@ -0,0 +1,83 @@
+import gql from 'graphql-tag';
+
+export const ADJUSTMENT_OPERATION_FRAGMENT = gql`
+    fragment AdjustmentOperation on AdjustmentOperation {
+        args {
+            name
+            type
+            value
+        }
+        code
+        description
+    }
+`;
+
+export const PROMOTION_FRAGMENT = gql`
+    fragment Promotion on Promotion {
+        id
+        createdAt
+        updatedAt
+        name
+        enabled
+        conditions {
+            ...AdjustmentOperation
+        }
+        actions {
+            ...AdjustmentOperation
+        }
+    }
+    ${ADJUSTMENT_OPERATION_FRAGMENT}
+`;
+
+export const GET_PROMOTION_LIST = gql`
+    query GetPromotionList($options: PromotionListOptions) {
+        promotions(options: $options) {
+            items {
+                ...Promotion
+            }
+            totalItems
+        }
+    }
+    ${PROMOTION_FRAGMENT}
+`;
+
+export const GET_PROMOTION = gql`
+    query GetPromotion($id: ID!) {
+        promotion(id: $id) {
+            ...Promotion
+        }
+    }
+    ${PROMOTION_FRAGMENT}
+`;
+
+export const GET_ADJUSTMENT_OPERATIONS = gql`
+    query GetAdjustmentOperations {
+        adjustmentOperations {
+            actions {
+                ...AdjustmentOperation
+            }
+            conditions {
+                ...AdjustmentOperation
+            }
+        }
+    }
+    ${ADJUSTMENT_OPERATION_FRAGMENT}
+`;
+
+export const CREATE_PROMOTION = gql`
+    mutation CreatePromotion($input: CreatePromotionInput!) {
+        createPromotion(input: $input) {
+            ...Promotion
+        }
+    }
+    ${PROMOTION_FRAGMENT}
+`;
+
+export const UPDATE_PROMOTION = gql`
+    mutation UpdatePromotion($input: UpdatePromotionInput!) {
+        updatePromotion(input: $input) {
+            ...Promotion
+        }
+    }
+    ${PROMOTION_FRAGMENT}
+`;

+ 0 - 89
admin-ui/src/app/data/providers/adjustment-source-data.service.ts

@@ -1,89 +0,0 @@
-import {
-    AdjustmentType,
-    CreateAdjustmentSource,
-    CreateAdjustmentSourceInput,
-    GetAdjustmentOperations,
-    GetAdjustmentSource,
-    GetAdjustmentSourceList,
-    UpdateAdjustmentSource,
-    UpdateAdjustmentSourceInput,
-} from 'shared/generated-types';
-
-import {
-    CREATE_ADJUSTMENT_SOURCE,
-    GET_ADJUSTMENT_OPERATIONS,
-    GET_ADJUSTMENT_SOURCE,
-    GET_ADJUSTMENT_SOURCE_LIST,
-    UPDATE_ADJUSTMENT_SOURCE,
-} from '../definitions/adjustment-source-definitions';
-
-import { BaseDataService } from './base-data.service';
-
-export class AdjustmentSourceDataService {
-    constructor(private baseDataService: BaseDataService) {}
-
-    getPromotions(take: number = 10, skip: number = 0) {
-        return this.getAdjustmentSourceList(AdjustmentType.PROMOTION, take, skip);
-    }
-
-    getPromotion(id: string) {
-        return this.getAdjustmentSource(AdjustmentType.PROMOTION, id);
-    }
-
-    getAdjustmentOperations(type: AdjustmentType) {
-        return this.baseDataService.query<GetAdjustmentOperations.Query, GetAdjustmentOperations.Variables>(
-            GET_ADJUSTMENT_OPERATIONS,
-            {
-                type,
-            },
-        );
-    }
-
-    createPromotion(input: CreateAdjustmentSourceInput) {
-        return this.createAdjustmentSource(input);
-    }
-
-    updatePromotion(input: UpdateAdjustmentSourceInput) {
-        return this.updateAdjustmentSource(input);
-    }
-
-    private getAdjustmentSourceList(type: AdjustmentType, take: number, skip: number) {
-        return this.baseDataService.query<GetAdjustmentSourceList.Query, GetAdjustmentSourceList.Variables>(
-            GET_ADJUSTMENT_SOURCE_LIST,
-            {
-                type,
-                options: {
-                    take,
-                    skip,
-                },
-            },
-        );
-    }
-
-    private getAdjustmentSource(type: AdjustmentType, id: string) {
-        return this.baseDataService.query<GetAdjustmentSource.Query, GetAdjustmentSource.Variables>(
-            GET_ADJUSTMENT_SOURCE,
-            {
-                id,
-            },
-        );
-    }
-
-    private createAdjustmentSource(input: CreateAdjustmentSourceInput) {
-        return this.baseDataService.mutate<CreateAdjustmentSource.Mutation, CreateAdjustmentSource.Variables>(
-            CREATE_ADJUSTMENT_SOURCE,
-            {
-                input,
-            },
-        );
-    }
-
-    private updateAdjustmentSource(input: UpdateAdjustmentSourceInput) {
-        return this.baseDataService.mutate<UpdateAdjustmentSource.Mutation, UpdateAdjustmentSource.Variables>(
-            UPDATE_ADJUSTMENT_SOURCE,
-            {
-                input,
-            },
-        );
-    }
-}

+ 1 - 1
admin-ui/src/app/data/providers/data.service.mock.ts

@@ -30,7 +30,7 @@ export function spyObservable(name: string, returnValue: any = {}): jasmine.Spy
 }
 
 export class MockDataService implements DataServiceMock {
-    adjustmentSource = {
+    promotion = {
         getPromotions: spyQueryResult('getPromotions'),
         getPromotion: spyQueryResult('getPromotion'),
         getAdjustmentOperations: spyQueryResult('getAdjustmentOperations'),

+ 3 - 3
admin-ui/src/app/data/providers/data.service.ts

@@ -1,6 +1,5 @@
 import { Injectable } from '@angular/core';
 
-import { AdjustmentSourceDataService } from './adjustment-source-data.service';
 import { AdministratorDataService } from './administrator-data.service';
 import { AuthDataService } from './auth-data.service';
 import { BaseDataService } from './base-data.service';
@@ -8,11 +7,12 @@ import { ClientDataService } from './client-data.service';
 import { FacetDataService } from './facet-data.service';
 import { OrderDataService } from './order-data.service';
 import { ProductDataService } from './product-data.service';
+import { PromotionDataService } from './promotion-data.service';
 import { SettingsDataService } from './settings-data.service';
 
 @Injectable()
 export class DataService {
-    adjustmentSource: AdjustmentSourceDataService;
+    promotion: PromotionDataService;
     administrator: AdministratorDataService;
     auth: AuthDataService;
     product: ProductDataService;
@@ -22,7 +22,7 @@ export class DataService {
     settings: SettingsDataService;
 
     constructor(baseDataService: BaseDataService) {
-        this.adjustmentSource = new AdjustmentSourceDataService(baseDataService);
+        this.promotion = new PromotionDataService(baseDataService);
         this.administrator = new AdministratorDataService(baseDataService);
         this.auth = new AuthDataService(baseDataService);
         this.product = new ProductDataService(baseDataService);

+ 63 - 0
admin-ui/src/app/data/providers/promotion-data.service.ts

@@ -0,0 +1,63 @@
+import {
+    CreatePromotion,
+    CreatePromotionInput,
+    GetAdjustmentOperations,
+    GetPromotion,
+    GetPromotionList,
+    UpdatePromotion,
+    UpdatePromotionInput,
+} from 'shared/generated-types';
+
+import {
+    CREATE_PROMOTION,
+    GET_ADJUSTMENT_OPERATIONS,
+    GET_PROMOTION,
+    GET_PROMOTION_LIST,
+    UPDATE_PROMOTION,
+} from '../definitions/promotion-definitions';
+
+import { BaseDataService } from './base-data.service';
+
+export class PromotionDataService {
+    constructor(private baseDataService: BaseDataService) {}
+
+    getPromotions(take: number = 10, skip: number = 0) {
+        return this.baseDataService.query<GetPromotionList.Query, GetPromotionList.Variables>(
+            GET_PROMOTION_LIST,
+            {
+                options: {
+                    take,
+                    skip,
+                },
+            },
+        );
+    }
+
+    getPromotion(id: string) {
+        return this.baseDataService.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
+            id,
+        });
+    }
+
+    getAdjustmentOperations() {
+        return this.baseDataService.query<GetAdjustmentOperations.Query>(GET_ADJUSTMENT_OPERATIONS);
+    }
+
+    createPromotion(input: CreatePromotionInput) {
+        return this.baseDataService.mutate<CreatePromotion.Mutation, CreatePromotion.Variables>(
+            CREATE_PROMOTION,
+            {
+                input,
+            },
+        );
+    }
+
+    updatePromotion(input: UpdatePromotionInput) {
+        return this.baseDataService.mutate<UpdatePromotion.Mutation, UpdatePromotion.Variables>(
+            UPDATE_PROMOTION,
+            {
+                input,
+            },
+        );
+    }
+}

+ 17 - 21
admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts

@@ -6,11 +6,10 @@ import { mergeMap, take } from 'rxjs/operators';
 import {
     AdjustmentOperation,
     AdjustmentOperationInput,
-    AdjustmentSource,
-    AdjustmentType,
-    CreateAdjustmentSourceInput,
+    CreatePromotionInput,
     LanguageCode,
-    UpdateAdjustmentSourceInput,
+    Promotion,
+    UpdatePromotionInput,
 } from 'shared/generated-types';
 
 import { BaseDetailComponent } from '../../../common/base-detail.component';
@@ -25,9 +24,9 @@ import { ServerConfigService } from '../../../data/server-config';
     styleUrls: ['./promotion-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class PromotionDetailComponent extends BaseDetailComponent<AdjustmentSource.Fragment>
+export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Fragment>
     implements OnInit, OnDestroy {
-    promotion$: Observable<AdjustmentSource.Fragment>;
+    promotion$: Observable<Promotion.Fragment>;
     promotionForm: FormGroup;
     conditions: AdjustmentOperation[] = [];
     actions: AdjustmentOperation[] = [];
@@ -55,12 +54,10 @@ export class PromotionDetailComponent extends BaseDetailComponent<AdjustmentSour
     ngOnInit() {
         this.init();
         this.promotion$ = this.entity$;
-        const allOperations$ = this.dataService.adjustmentSource
-            .getAdjustmentOperations(AdjustmentType.PROMOTION)
-            .single$.subscribe(data => {
-                this.allActions = data.adjustmentOperations.actions;
-                this.allConditions = data.adjustmentOperations.conditions;
-            });
+        this.dataService.promotion.getAdjustmentOperations().single$.subscribe(data => {
+            this.allActions = data.adjustmentOperations.actions;
+            this.allConditions = data.adjustmentOperations.conditions;
+        });
     }
 
     ngOnDestroy() {
@@ -113,19 +110,18 @@ export class PromotionDetailComponent extends BaseDetailComponent<AdjustmentSour
             return;
         }
         const formValue = this.promotionForm.value;
-        const input: CreateAdjustmentSourceInput = {
+        const input: CreatePromotionInput = {
             name: formValue.name,
-            type: AdjustmentType.PROMOTION,
             enabled: true,
-            conditions: this.mapOperationsToInputs(this.conditions, formValue),
-            actions: this.mapOperationsToInputs(this.actions, formValue),
+            conditions: this.mapOperationsToInputs(this.conditions, formValue.conditions),
+            actions: this.mapOperationsToInputs(this.actions, formValue.actions),
         };
-        this.dataService.adjustmentSource.createPromotion(input).subscribe(
+        this.dataService.promotion.createPromotion(input).subscribe(
             data => {
                 this.notificationService.success(_('common.notify-create-success'), { entity: 'Promotion' });
                 this.promotionForm.markAsPristine();
                 this.changeDetector.markForCheck();
-                this.router.navigate(['../', data.createAdjustmentSource.id], { relativeTo: this.route });
+                this.router.navigate(['../', data.createPromotion.id], { relativeTo: this.route });
             },
             err => {
                 this.notificationService.error(_('common.notify-create-error'), {
@@ -144,13 +140,13 @@ export class PromotionDetailComponent extends BaseDetailComponent<AdjustmentSour
             .pipe(
                 take(1),
                 mergeMap(promotion => {
-                    const input: UpdateAdjustmentSourceInput = {
+                    const input: UpdatePromotionInput = {
                         id: promotion.id,
                         name: formValue.name,
                         conditions: this.mapOperationsToInputs(this.conditions, formValue.conditions),
                         actions: this.mapOperationsToInputs(this.actions, formValue.actions),
                     };
-                    return this.dataService.adjustmentSource.updatePromotion(input);
+                    return this.dataService.promotion.updatePromotion(input);
                 }),
             )
             .subscribe(
@@ -172,7 +168,7 @@ export class PromotionDetailComponent extends BaseDetailComponent<AdjustmentSour
     /**
      * Update the form values when the entity changes.
      */
-    protected setFormValues(entity: AdjustmentSource.Fragment, languageCode: LanguageCode): void {
+    protected setFormValues(entity: Promotion.Fragment, languageCode: LanguageCode): void {
         this.promotionForm.patchValue({ name: entity.name });
         entity.conditions.forEach(o => {
             this.addOperation('conditions', o);

+ 5 - 5
admin-ui/src/app/marketing/components/promotion-list/promotion-list.component.ts

@@ -1,6 +1,6 @@
 import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
-import { GetAdjustmentSourceList } from 'shared/generated-types';
+import { GetPromotionList } from 'shared/generated-types';
 
 import { BaseListComponent } from '../../../common/base-list.component';
 import { DataService } from '../../../data/providers/data.service';
@@ -12,14 +12,14 @@ import { DataService } from '../../../data/providers/data.service';
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class PromotionListComponent extends BaseListComponent<
-    GetAdjustmentSourceList.Query,
-    GetAdjustmentSourceList.Items
+    GetPromotionList.Query,
+    GetPromotionList.Items
 > {
     constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
         super(router, route);
         super.setQueryFn(
-            (...args: any[]) => this.dataService.adjustmentSource.getPromotions(...args),
-            data => data.adjustmentSources,
+            (...args: any[]) => this.dataService.promotion.getPromotions(...args),
+            data => data.promotions,
         );
     }
 }

+ 2 - 2
admin-ui/src/app/marketing/marketing.routes.ts

@@ -1,5 +1,5 @@
 import { Route } from '@angular/router';
-import { AdjustmentSource } from 'shared/generated-types';
+import { Promotion } from 'shared/generated-types';
 
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
@@ -28,7 +28,7 @@ export const marketingRoutes: Route[] = [
 ];
 
 export function promotionBreadcrumb(data: any, params: any) {
-    return detailBreadcrumb<AdjustmentSource.Fragment>({
+    return detailBreadcrumb<Promotion.Fragment>({
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.promotions',

+ 4 - 5
admin-ui/src/app/marketing/providers/routing/promotion-resolver.ts

@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core';
-import { AdjustmentSource, AdjustmentType } from 'shared/generated-types';
+import { Promotion } from 'shared/generated-types';
 
 import { BaseEntityResolver } from '../../../common/base-entity-resolver';
 import { DataService } from '../../../data/providers/data.service';
@@ -8,21 +8,20 @@ import { DataService } from '../../../data/providers/data.service';
  * Resolves the id from the path into a Customer entity.
  */
 @Injectable()
-export class PromotionResolver extends BaseEntityResolver<AdjustmentSource.Fragment> {
+export class PromotionResolver extends BaseEntityResolver<Promotion.Fragment> {
     constructor(private dataService: DataService) {
         super(
             {
-                __typename: 'AdjustmentSource',
+                __typename: 'Promotion',
                 id: '',
                 createdAt: '',
                 updatedAt: '',
-                type: AdjustmentType.PROMOTION,
                 name: '',
                 enabled: false,
                 conditions: [],
                 actions: [],
             },
-            id => this.dataService.adjustmentSource.getPromotion(id).mapStream(data => data.adjustmentSource),
+            id => this.dataService.promotion.getPromotion(id).mapStream(data => data.promotion),
         );
     }
 }

+ 0 - 5
admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.html

@@ -20,10 +20,5 @@
         <vdr-form-field [label]="'common.name' | translate" for="name">
             <input id="name" type="text" formControlName="name">
         </vdr-form-field>
-        <vdr-form-field [label]="'settings.tax-rate' | translate" for="name">
-            <vdr-affixed-input suffix="%">
-                <input id="name" type="number" step="0.1" min="0" max="100" formControlName="taxRate">
-            </vdr-affixed-input>
-        </vdr-form-field>
     </section>
 </form>

+ 4 - 19
admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.ts

@@ -3,14 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { Observable } from 'rxjs';
 import { mergeMap, take } from 'rxjs/operators';
-import {
-    AdjustmentOperation,
-    AdjustmentSource,
-    AdjustmentType,
-    CreateAdjustmentSourceInput,
-    LanguageCode,
-    UpdateAdjustmentSourceInput,
-} from 'shared/generated-types';
+import { AdjustmentOperation, LanguageCode, TaxCategory } from 'shared/generated-types';
 
 import { BaseDetailComponent } from '../../../common/base-detail.component';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
@@ -24,9 +17,9 @@ import { ServerConfigService } from '../../../data/server-config';
     styleUrls: ['./tax-category-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class TaxCategoryDetailComponent extends BaseDetailComponent<AdjustmentSource.Fragment>
+export class TaxCategoryDetailComponent extends BaseDetailComponent<TaxCategory.Fragment>
     implements OnInit, OnDestroy {
-    taxCategory$: Observable<AdjustmentSource.Fragment>;
+    taxCategory$: Observable<TaxCategory.Fragment>;
     taxCategoryForm: FormGroup;
 
     private taxCondition: AdjustmentOperation;
@@ -51,12 +44,6 @@ export class TaxCategoryDetailComponent extends BaseDetailComponent<AdjustmentSo
     ngOnInit() {
         this.init();
         this.taxCategory$ = this.entity$;
-        const allOperations$ = this.dataService.adjustmentSource
-            .getAdjustmentOperations(AdjustmentType.TAX)
-            .single$.subscribe(data => {
-                this.taxCondition = data.adjustmentOperations.conditions[0];
-                this.taxAction = data.adjustmentOperations.actions[0];
-            });
     }
 
     ngOnDestroy() {
@@ -125,11 +112,9 @@ export class TaxCategoryDetailComponent extends BaseDetailComponent<AdjustmentSo
     /**
      * Update the form values when the entity changes.
      */
-    protected setFormValues(entity: AdjustmentSource.Fragment, languageCode: LanguageCode): void {
-        const action = entity.actions[0];
+    protected setFormValues(entity: TaxCategory.Fragment, languageCode: LanguageCode): void {
         this.taxCategoryForm.patchValue({
             name: entity.name,
-            taxRate: action ? action.args[0].value : 0,
         });
     }
 }

+ 0 - 2
admin-ui/src/app/settings/components/tax-category-list/tax-category-list.component.html

@@ -10,12 +10,10 @@
 <vdr-data-table [items]="taxCategories$ | async">
     <vdr-dt-column>{{ 'common.ID' | translate }}</vdr-dt-column>
     <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'settings.tax-rate' | translate }}</vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
     <ng-template let-taxCategory="item">
         <td class="left">{{ taxCategory.id }}</td>
         <td class="left">{{ taxCategory.name }}</td>
-        <td class="left">{{ taxCategory.actions[0].args[0].value }}%</td>
         <td class="right">
             <vdr-table-row-action iconShape="edit"
                                   [label]="'common.edit' | translate"

+ 1 - 1
admin-ui/src/app/settings/providers/routing/country-resolver.ts

@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core';
-import { AdjustmentSource, AdjustmentType, Country } from 'shared/generated-types';
+import { Country } from 'shared/generated-types';
 
 import { BaseEntityResolver } from '../../../common/base-entity-resolver';
 import { DataService } from '../../../data/providers/data.service';

+ 4 - 10
admin-ui/src/app/settings/providers/routing/tax-category-resolver.ts

@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core';
-import { AdjustmentSource, AdjustmentType } from 'shared/generated-types';
+import { TaxCategory } from 'shared/generated-types';
 
 import { BaseEntityResolver } from '../../../common/base-entity-resolver';
 import { DataService } from '../../../data/providers/data.service';
@@ -8,21 +8,15 @@ import { DataService } from '../../../data/providers/data.service';
  * Resolves the id from the path into a Customer entity.
  */
 @Injectable()
-export class TaxCategoryResolver extends BaseEntityResolver<AdjustmentSource.Fragment> {
+export class TaxCategoryResolver extends BaseEntityResolver<TaxCategory.Fragment> {
     constructor(private dataService: DataService) {
         super(
             {
-                __typename: 'AdjustmentSource',
+                __typename: 'TaxCategory',
                 id: '',
-                createdAt: '',
-                updatedAt: '',
-                type: AdjustmentType.TAX,
                 name: '',
-                enabled: false,
-                conditions: [],
-                actions: [],
             },
-            id => this.dataService.adjustmentSource.getPromotion(id).mapStream(data => data.adjustmentSource),
+            id => this.dataService.settings.getTaxCategory(id).mapStream(data => data.taxCategory),
         );
     }
 }

+ 3 - 3
admin-ui/src/app/settings/settings.routes.ts

@@ -1,5 +1,5 @@
 import { Route } from '@angular/router';
-import { AdjustmentSource, Administrator, Role } from 'shared/generated-types';
+import { Administrator, Country, GetCountry, Role, TaxCategory } from 'shared/generated-types';
 
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
@@ -98,7 +98,7 @@ export function roleBreadcrumb(data: any, params: any) {
 }
 
 export function taxCategoryBreadcrumb(data: any, params: any) {
-    return detailBreadcrumb<AdjustmentSource.Fragment>({
+    return detailBreadcrumb<TaxCategory.Fragment>({
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.tax-categories',
@@ -108,7 +108,7 @@ export function taxCategoryBreadcrumb(data: any, params: any) {
 }
 
 export function countryBreadcrumb(data: any, params: any) {
-    return detailBreadcrumb<AdjustmentSource.Fragment>({
+    return detailBreadcrumb<Country.Fragment>({
         entity: data.entity,
         id: params.id,
         breadcrumbKey: 'breadcrumb.countries',

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 7 - 7
server/e2e/adjustment-source.e2e-spec.ts

@@ -15,7 +15,7 @@ import {
     GET_ADJUSTMENT_SOURCE,
     GET_ADJUSTMENT_SOURCE_LIST,
     UPDATE_ADJUSTMENT_SOURCE,
-} from '../../admin-ui/src/app/data/definitions/adjustment-source-definitions';
+} from '../../admin-ui/src/app/data/definitions/promotion-definitions';
 import {
     AdjustmentActionDefinition,
     AdjustmentConditionDefinition,
@@ -62,7 +62,7 @@ describe('AdjustmentSource resolver', () => {
         await server.destroy();
     });
 
-    it('createAdjustmentSource promotion', async () => {
+    it('createPromotion promotion', async () => {
         const result = await client.query<CreateAdjustmentSource.Mutation, CreateAdjustmentSource.Variables>(
             CREATE_ADJUSTMENT_SOURCE,
             {
@@ -89,7 +89,7 @@ describe('AdjustmentSource resolver', () => {
         expect(pick(promoAdjustmentSource, snapshotProps)).toMatchSnapshot();
     });
 
-    it('createAdjustmentSource tax', async () => {
+    it('createPromotion tax', async () => {
         const result = await client.query<CreateAdjustmentSource.Mutation, CreateAdjustmentSource.Variables>(
             CREATE_ADJUSTMENT_SOURCE,
             {
@@ -115,7 +115,7 @@ describe('AdjustmentSource resolver', () => {
         expect(pick(result.createAdjustmentSource, snapshotProps)).toMatchSnapshot();
     });
 
-    it('createAdjustmentSource shipping', async () => {
+    it('createPromotion shipping', async () => {
         const result = await client.query<CreateAdjustmentSource.Mutation, CreateAdjustmentSource.Variables>(
             CREATE_ADJUSTMENT_SOURCE,
             {
@@ -141,7 +141,7 @@ describe('AdjustmentSource resolver', () => {
         expect(pick(result.createAdjustmentSource, snapshotProps)).toMatchSnapshot();
     });
 
-    it('updateAdjustmentSource', async () => {
+    it('updatePromotion', async () => {
         const result = await client.query<UpdateAdjustmentSource.Mutation, UpdateAdjustmentSource.Variables>(
             UPDATE_ADJUSTMENT_SOURCE,
             {
@@ -163,7 +163,7 @@ describe('AdjustmentSource resolver', () => {
         expect(pick(result.updateAdjustmentSource, snapshotProps)).toMatchSnapshot();
     });
 
-    it('adjustmentSource', async () => {
+    it('promotion', async () => {
         const result = await client.query<GetAdjustmentSource.Query, GetAdjustmentSource.Variables>(
             GET_ADJUSTMENT_SOURCE,
             {
@@ -171,7 +171,7 @@ describe('AdjustmentSource resolver', () => {
             },
         );
 
-        expect(result.adjustmentSource!.name).toBe(promoAdjustmentSource.name);
+        expect(result.promotion!.name).toBe(promoAdjustmentSource.name);
     });
 
     it('adjustmentSources, type = promotion', async () => {

+ 3 - 3
server/e2e/order.e2e-spec.ts

@@ -1,7 +1,7 @@
 import gql from 'graphql-tag';
 
 import { Customer } from '../src/entity/customer/customer.entity';
-import { OrderItem } from '../src/entity/order-item/order-item.entity';
+import { OrderLine } from '../src/entity/order-line/order-line.entity';
 
 import { TestClient } from './test-client';
 import { TestServer } from './test-server';
@@ -78,7 +78,7 @@ describe('Orders', () => {
             }
         });
 
-        it('addItemToOrder() with an existing productVariantId adds quantity to the existing OrderItem', async () => {
+        it('addItemToOrder() with an existing productVariantId adds quantity to the existing OrderLine', async () => {
             const result = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
                 quantity: 2,
@@ -184,7 +184,7 @@ describe('Orders', () => {
             firstOrderItemId = result.addItemToOrder.items[0].id;
         });
 
-        it('addItemToOrder() with an existing productVariantId adds quantity to the existing OrderItem', async () => {
+        it('addItemToOrder() with an existing productVariantId adds quantity to the existing OrderLine', async () => {
             const result = await client.query(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
                 quantity: 2,

+ 0 - 4
server/mock-data/mock-data.service.ts

@@ -4,12 +4,9 @@ import gql from 'graphql-tag';
 import * as path from 'path';
 import {
     AddOptionGroupToProduct,
-    AdjustmentSource,
-    AdjustmentType,
     Asset,
     Country,
     CreateAddressInput,
-    CreateAdjustmentSource,
     CreateCountry,
     CreateCustomerInput,
     CreateFacet,
@@ -24,7 +21,6 @@ import {
     UpdateProductVariants,
 } from 'shared/generated-types';
 
-import { CREATE_ADJUSTMENT_SOURCE } from '../../admin-ui/src/app/data/definitions/adjustment-source-definitions';
 import { CREATE_FACET } from '../../admin-ui/src/app/data/definitions/facet-definitions';
 import {
     ADD_OPTION_GROUP_TO_PRODUCT,

+ 2 - 2
server/src/api/api.module.ts

@@ -12,7 +12,6 @@ import { AuthGuard } from './common/auth-guard';
 import { GraphqlConfigService } from './common/graphql-config.service';
 import { IdInterceptor } from './common/id-interceptor';
 import { RequestContextService } from './common/request-context.service';
-import { AdjustmentSourceResolver } from './resolvers/adjustment-source.resolver';
 import { AdministratorResolver } from './resolvers/administrator.resolver';
 import { AssetResolver } from './resolvers/asset.resolver';
 import { AuthResolver } from './resolvers/auth.resolver';
@@ -25,12 +24,13 @@ import { FacetResolver } from './resolvers/facet.resolver';
 import { OrderResolver } from './resolvers/order.resolver';
 import { ProductOptionResolver } from './resolvers/product-option.resolver';
 import { ProductResolver } from './resolvers/product.resolver';
+import { PromotionResolver } from './resolvers/promotion.resolver';
 import { RoleResolver } from './resolvers/role.resolver';
 import { TaxCategoryResolver } from './resolvers/tax-category.resolver';
 import { ZoneResolver } from './resolvers/zone.resolver';
 
 const exportedProviders = [
-    AdjustmentSourceResolver,
+    PromotionResolver,
     AdministratorResolver,
     AuthResolver,
     AssetResolver,

+ 0 - 73
server/src/api/resolvers/adjustment-source.resolver.ts

@@ -1,73 +0,0 @@
-import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
-import {
-    AdjustmentOperationsQueryArgs,
-    AdjustmentSourceQueryArgs,
-    AdjustmentSourcesQueryArgs,
-    CreateAdjustmentSourceMutationArgs,
-    Permission,
-    UpdateAdjustmentSourceMutationArgs,
-} from 'shared/generated-types';
-import { PaginatedList } from 'shared/shared-types';
-
-import { AdjustmentSource } from '../../entity/adjustment-source/adjustment-source.entity';
-import { Order } from '../../entity/order/order.entity';
-import { AdjustmentSourceService } from '../../service/providers/adjustment-source.service';
-import { Allow } from '../common/auth-guard';
-import { RequestContext } from '../common/request-context';
-import { Ctx } from '../common/request-context.decorator';
-
-@Resolver('Order')
-export class AdjustmentSourceResolver {
-    constructor(private adjustmentSourceService: AdjustmentSourceService) {}
-
-    @Query()
-    @Allow(Permission.ReadAdjustmentSource)
-    adjustmentSources(
-        @Ctx() ctx: RequestContext,
-        @Args() args: AdjustmentSourcesQueryArgs,
-    ): Promise<PaginatedList<AdjustmentSource>> {
-        if (!args.options) {
-            args.options = {};
-        }
-        if (!args.options.filter) {
-            args.options.filter = {};
-        }
-        args.options.filter.type = {
-            eq: args.type,
-        };
-        return this.adjustmentSourceService.findAll(args.options || undefined);
-    }
-
-    @Query()
-    @Allow(Permission.ReadAdjustmentSource)
-    adjustmentSource(
-        @Ctx() ctx: RequestContext,
-        @Args() args: AdjustmentSourceQueryArgs,
-    ): Promise<AdjustmentSource | undefined> {
-        return this.adjustmentSourceService.findOne(args.id);
-    }
-
-    @Query()
-    @Allow(Permission.ReadAdjustmentSource)
-    adjustmentOperations(@Ctx() ctx: RequestContext, @Args() args: AdjustmentOperationsQueryArgs) {
-        return this.adjustmentSourceService.getAdjustmentOperations(args.type);
-    }
-
-    @Mutation()
-    @Allow(Permission.CreateAdjustmentSource)
-    createAdjustmentSource(
-        @Ctx() ctx: RequestContext,
-        @Args() args: CreateAdjustmentSourceMutationArgs,
-    ): Promise<AdjustmentSource> {
-        return this.adjustmentSourceService.createAdjustmentSource(ctx, args.input);
-    }
-
-    @Mutation()
-    @Allow(Permission.UpdateAdjustmentSource)
-    updateAdjustmentSource(
-        @Ctx() ctx: RequestContext,
-        @Args() args: UpdateAdjustmentSourceMutationArgs,
-    ): Promise<AdjustmentSource> {
-        return this.adjustmentSourceService.updateAdjustmentSource(ctx, args.input);
-    }
-}

+ 59 - 0
server/src/api/resolvers/promotion.resolver.ts

@@ -0,0 +1,59 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import {
+    CreatePromotionMutationArgs,
+    Permission,
+    PromotionQueryArgs,
+    PromotionsQueryArgs,
+    UpdatePromotionMutationArgs,
+} from 'shared/generated-types';
+import { PaginatedList } from 'shared/shared-types';
+
+import { Promotion } from '../../entity/promotion/promotion.entity';
+import { PromotionService } from '../../service/providers/promotion.service';
+import { Allow } from '../common/auth-guard';
+import { RequestContext } from '../common/request-context';
+import { Ctx } from '../common/request-context.decorator';
+
+@Resolver('Promotion')
+export class PromotionResolver {
+    constructor(private promotionService: PromotionService) {}
+
+    @Query()
+    @Allow(Permission.ReadSettings)
+    promotions(
+        @Ctx() ctx: RequestContext,
+        @Args() args: PromotionsQueryArgs,
+    ): Promise<PaginatedList<Promotion>> {
+        return this.promotionService.findAll(args.options || undefined);
+    }
+
+    @Query()
+    @Allow(Permission.ReadSettings)
+    promotion(@Ctx() ctx: RequestContext, @Args() args: PromotionQueryArgs): Promise<Promotion | undefined> {
+        return this.promotionService.findOne(args.id);
+    }
+
+    @Query()
+    @Allow(Permission.ReadSettings)
+    adjustmentOperations(@Ctx() ctx: RequestContext) {
+        return this.promotionService.getAdjustmentOperations();
+    }
+
+    @Mutation()
+    @Allow(Permission.CreateSettings)
+    createPromotion(
+        @Ctx() ctx: RequestContext,
+        @Args() args: CreatePromotionMutationArgs,
+    ): Promise<Promotion> {
+        return this.promotionService.createPromotion(ctx, args.input);
+    }
+
+    @Mutation()
+    @Allow(Permission.UpdateSettings)
+    updatePromotion(
+        @Ctx() ctx: RequestContext,
+        @Args() args: UpdatePromotionMutationArgs,
+    ): Promise<Promotion> {
+        return this.promotionService.updatePromotion(ctx, args.input);
+    }
+}

+ 0 - 41
server/src/api/types/adjustment-source.api.graphql

@@ -1,41 +0,0 @@
-type Query {
-    adjustmentSource(id: ID!): AdjustmentSource
-    adjustmentSources(type: AdjustmentType!, options: AdjustmentSourceListOptions): AdjustmentSourceList!
-    adjustmentOperations(type: AdjustmentType!): AdjustmentOperations!
-}
-
-type AdjustmentOperations {
-    conditions: [AdjustmentOperation!]!
-    actions: [AdjustmentOperation!]!
-}
-
-type Mutation {
-    createAdjustmentSource(input: CreateAdjustmentSourceInput!): AdjustmentSource!
-    updateAdjustmentSource(input: UpdateAdjustmentSourceInput!): AdjustmentSource!
-}
-
-type AdjustmentSourceList implements PaginatedList {
-    items: [AdjustmentSource!]!
-    totalItems: Int!
-}
-
-input AdjustmentSourceListOptions {
-    take: Int
-    skip: Int
-    sort: AdjustmentSourceSortParameter
-    filter: AdjustmentSourceFilterParameter
-}
-
-input AdjustmentSourceSortParameter {
-    id: SortOrder
-    createdAt: SortOrder
-    updatedAt: SortOrder
-    name: SortOrder
-}
-
-input AdjustmentSourceFilterParameter {
-    name: StringOperators
-    createdAt: DateOperators
-    updatedAt: DateOperators
-    type: StringOperators
-}

+ 41 - 0
server/src/api/types/promotion.api.graphql

@@ -0,0 +1,41 @@
+type Query {
+    promotion(id: ID!): Promotion
+    promotions(options: PromotionListOptions): PromotionList!
+    adjustmentOperations: AdjustmentOperations!
+}
+
+type AdjustmentOperations {
+    conditions: [AdjustmentOperation!]!
+    actions: [AdjustmentOperation!]!
+}
+
+type Mutation {
+    createPromotion(input: CreatePromotionInput!): Promotion!
+    updatePromotion(input: UpdatePromotionInput!): Promotion!
+}
+
+type PromotionList implements PaginatedList {
+    items: [Promotion!]!
+    totalItems: Int!
+}
+
+input PromotionListOptions {
+    take: Int
+    skip: Int
+    sort: PromotionSortParameter
+    filter: PromotionFilterParameter
+}
+
+input PromotionSortParameter {
+    id: SortOrder
+    createdAt: SortOrder
+    updatedAt: SortOrder
+    name: SortOrder
+}
+
+input PromotionFilterParameter {
+    name: StringOperators
+    createdAt: DateOperators
+    updatedAt: DateOperators
+    type: StringOperators
+}

+ 18 - 0
server/src/common/calculated-decorator.ts

@@ -0,0 +1,18 @@
+export const CALCULATED_PROPERTIES = '__calculatedProperties__';
+
+/**
+ * Used to define calculated entity getters. The decorator simply attaches an array of "calculated"
+ * property names to the entity's prototype. This array is then used by the {@link CalculatedPropertySubscriber}
+ * to transfer the getter function from the prototype to the entity instance.
+ */
+export function Calculated(): MethodDecorator {
+    return (target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
+        if (target[CALCULATED_PROPERTIES]) {
+            if (!target[CALCULATED_PROPERTIES].includes(propertyKey)) {
+                target[CALCULATED_PROPERTIES].push(propertyKey);
+            }
+        } else {
+            target[CALCULATED_PROPERTIES] = [propertyKey];
+        }
+    };
+}

+ 1 - 1
server/src/common/types/adjustment-source.ts

@@ -6,7 +6,7 @@ export interface AdjustmentSource {
 }
 
 /**
- * When an AdjustmentSource is applied to an OrderItem or Order, an Adjustment is
+ * When an AdjustmentSource is applied to an OrderItem, an Adjustment is
  * generated based on the actions assigned to the AdjustmentSource.
  */
 export interface Adjustment {

+ 6 - 0
server/src/common/types/common-types.graphql

@@ -3,6 +3,12 @@ scalar JSON
 scalar DateTime
 scalar Upload
 
+type Adjustment {
+    promotionId: ID!
+    description: String!
+    amount: Int!
+}
+
 interface PaginatedList {
     items: [Node!]!
     totalItems: Int!

+ 1 - 5
server/src/config/adjustment/default-adjustment-actions.ts

@@ -1,9 +1,6 @@
-import { AdjustmentType } from 'shared/generated-types';
-
 import { AdjustmentActionDefinition } from './adjustment-types';
 
 export const orderPercentageDiscount: AdjustmentActionDefinition = {
-    type: AdjustmentType.PROMOTION,
     code: 'order_percentage_discount',
     args: [{ name: 'discount', type: 'percentage' }],
     calculate(order, args) {
@@ -13,11 +10,10 @@ export const orderPercentageDiscount: AdjustmentActionDefinition = {
 };
 
 export const itemPercentageDiscount: AdjustmentActionDefinition = {
-    type: AdjustmentType.PROMOTION,
     code: 'item_percentage_discount',
     args: [{ name: 'discount', type: 'percentage' }],
     calculate(order, args) {
-        return order.items.map(item => ({
+        return order.lines.map(item => ({
             orderItemId: item.id,
             amount: -(item.totalPrice * args.discount) / 100,
         }));

+ 1 - 6
server/src/config/adjustment/default-adjustment-conditions.ts

@@ -1,11 +1,8 @@
-import { AdjustmentType } from 'shared/generated-types';
-
 import { Order } from '../../entity/order/order.entity';
 
 import { AdjustmentConditionDefinition } from './adjustment-types';
 
 export const minimumOrderAmount: AdjustmentConditionDefinition = {
-    type: AdjustmentType.PROMOTION,
     code: 'minimum_order_amount',
     args: [{ name: 'amount', type: 'money' }],
     predicate(order: Order, args) {
@@ -15,7 +12,6 @@ export const minimumOrderAmount: AdjustmentConditionDefinition = {
 };
 
 export const dateRange: AdjustmentConditionDefinition = {
-    type: AdjustmentType.PROMOTION,
     code: 'date_range',
     args: [{ name: 'start', type: 'datetime' }, { name: 'end', type: 'datetime' }],
     predicate(order: Order, args) {
@@ -26,11 +22,10 @@ export const dateRange: AdjustmentConditionDefinition = {
 };
 
 export const atLeastNOfProduct: AdjustmentConditionDefinition = {
-    type: AdjustmentType.PROMOTION,
     code: 'at_least_n_of_product',
     args: [{ name: 'minimum', type: 'int' }],
     predicate(order: Order, args) {
-        return order.items.reduce((result, item) => {
+        return order.lines.reduce((result, item) => {
             return result || item.quantity >= args.minimum;
         }, false);
     },

+ 1 - 4
server/src/config/adjustment/required-adjustment-actions.ts

@@ -1,15 +1,12 @@
-import { AdjustmentType } from 'shared/generated-types';
-
 import { idsAreEqual } from '../../common/utils';
 
 import { AdjustmentActionDefinition, TaxActionDefinition } from './adjustment-types';
 
 export const taxAction: TaxActionDefinition = {
-    type: AdjustmentType.TAX,
     code: 'tax_action',
     args: [{ name: 'taxRate', type: 'percentage' }],
     calculate(order, args, context) {
-        return order.items
+        return order.lines
             .filter(item => idsAreEqual(item.taxCategoryId, context.taxCategoryId))
             .map(item => ({
                 orderItemId: item.id,

+ 0 - 3
server/src/config/adjustment/required-adjustment-conditions.ts

@@ -1,11 +1,8 @@
-import { AdjustmentType } from 'shared/generated-types';
-
 import { Order } from '../../entity/order/order.entity';
 
 import { AdjustmentConditionDefinition } from './adjustment-types';
 
 export const taxCondition: AdjustmentConditionDefinition = {
-    type: AdjustmentType.TAX,
     code: 'tax_condition',
     args: [],
     predicate(order: Order, args) {

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

@@ -1,70 +0,0 @@
-import { AdjustmentOperation, AdjustmentType } from 'shared/generated-types';
-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';
-
-@Entity()
-export class AdjustmentSource extends VendureEntity implements ChannelAware {
-    constructor(input?: DeepPartial<AdjustmentSource>) {
-        super(input);
-    }
-
-    @Column() name: string;
-
-    @Column() enabled: boolean;
-
-    @Column('varchar') type: AdjustmentType;
-
-    @ManyToMany(type => Channel)
-    @JoinTable()
-    channels: Channel[];
-
-    @Column('simple-json') conditions: AdjustmentOperation[];
-
-    @Column('simple-json') actions: AdjustmentOperation[];
-
-    /**
-     * A shorthand method for getting the tax rate on a TAX type adjustment source.
-     */
-    getTaxCategoryRate(): number {
-        if (this.type !== AdjustmentType.TAX) {
-            throw new I18nError(`error.getTaxCategoryRate-only-valid-for-tax-adjustment-sources`);
-        }
-        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(),
-                        },
-                    ],
-                },
-            ],
-        });
-    }
-}

+ 2 - 1
server/src/entity/entities.ts

@@ -1,5 +1,4 @@
 import { Address } from './address/address.entity';
-import { AdjustmentSource } from './adjustment-source/adjustment-source.entity';
 import { Administrator } from './administrator/administrator.entity';
 import { Asset } from './asset/asset.entity';
 import { Channel } from './channel/channel.entity';
@@ -11,6 +10,7 @@ import { FacetValue } from './facet-value/facet-value.entity';
 import { FacetTranslation } from './facet/facet-translation.entity';
 import { Facet } from './facet/facet.entity';
 import { OrderItem } from './order-item/order-item.entity';
+import { OrderLine } from './order-line/order-line.entity';
 import { Order } from './order/order.entity';
 import { ProductOptionGroupTranslation } from './product-option-group/product-option-group-translation.entity';
 import { ProductOptionGroup } from './product-option-group/product-option-group.entity';
@@ -49,6 +49,7 @@ export const coreEntitiesMap = {
     FacetValue,
     FacetValueTranslation,
     Order,
+    OrderLine,
     OrderItem,
     Product,
     ProductOption,

+ 6 - 31
server/src/entity/order-item/order-item.entity.ts

@@ -1,11 +1,9 @@
-import { DeepPartial, ID } from 'shared/shared-types';
+import { DeepPartial } from 'shared/shared-types';
 import { Column, Entity, ManyToOne } from 'typeorm';
 
 import { Adjustment } from '../../common/types/adjustment-source';
-import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
-import { Order } from '../order/order.entity';
-import { ProductVariant } from '../product-variant/product-variant.entity';
+import { OrderLine } from '../order-line/order-line.entity';
 
 @Entity()
 export class OrderItem extends VendureEntity {
@@ -13,32 +11,9 @@ export class OrderItem extends VendureEntity {
         super(input);
     }
 
-    @ManyToOne(type => ProductVariant)
-    productVariant: ProductVariant;
+    @ManyToOne(type => OrderLine, line => line.items)
+    line: OrderLine;
 
-    @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;
-
-    @Column() totalPriceBeforeAdjustment: number;
-
-    @Column() totalPrice: number;
-
-    @Column('simple-json') adjustments: Adjustment[];
-
-    @ManyToOne(type => Order, order => order.items)
-    order: Order;
+    /*@Column('simple-json')
+    pendingAdjustments: Adjustment[];*/
 }

+ 0 - 7
server/src/entity/order-item/order-item.graphql

@@ -2,11 +2,4 @@ type OrderItem implements Node {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
-    productVariant: ProductVariant!
-    adjustments: [Adjustment!]!
-    featuredAsset: Asset
-    unitPrice: Int!
-    quantity: Int!
-    totalPrice: Int!
-    order: Order!
 }

+ 43 - 0
server/src/entity/order-line/order-line.entity.ts

@@ -0,0 +1,43 @@
+import { DeepPartial, ID } from 'shared/shared-types';
+import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
+
+import { Calculated } from '../../common/calculated-decorator';
+import { Adjustment } from '../../common/types/adjustment-source';
+import { Asset } from '../asset/asset.entity';
+import { VendureEntity } from '../base/base.entity';
+import { OrderItem } from '../order-item/order-item.entity';
+import { Order } from '../order/order.entity';
+import { ProductVariant } from '../product-variant/product-variant.entity';
+
+@Entity()
+export class OrderLine extends VendureEntity {
+    constructor(input?: DeepPartial<OrderLine>) {
+        super(input);
+    }
+
+    @ManyToOne(type => ProductVariant)
+    productVariant: ProductVariant;
+
+    @Column('varchar') taxCategoryId: ID;
+
+    @ManyToOne(type => Asset)
+    featuredAsset: Asset;
+
+    @Column() unitPrice: number;
+
+    @OneToMany(type => OrderItem, item => item.line)
+    items: OrderItem[];
+
+    @ManyToOne(type => Order, order => order.lines)
+    order: Order;
+
+    @Calculated()
+    get quantity(): number {
+        return this.items ? this.items.length : 0;
+    }
+
+    @Calculated()
+    get totalPrice(): number {
+        return this.unitPrice * this.quantity;
+    }
+}

+ 12 - 0
server/src/entity/order-line/order-line.graphql

@@ -0,0 +1,12 @@
+type OrderLine implements Node {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    productVariant: ProductVariant!
+    featuredAsset: Asset
+    unitPrice: Int!
+    quantity: Int!
+    items: [OrderItem!]!
+    totalPrice: Int!
+    order: Order!
+}

+ 7 - 7
server/src/entity/order/order.entity.ts

@@ -4,7 +4,7 @@ import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
 import { Adjustment } from '../../common/types/adjustment-source';
 import { VendureEntity } from '../base/base.entity';
 import { Customer } from '../customer/customer.entity';
-import { OrderItem } from '../order-item/order-item.entity';
+import { OrderLine } from '../order-line/order-line.entity';
 
 @Entity()
 export class Order extends VendureEntity {
@@ -17,12 +17,12 @@ export class Order extends VendureEntity {
     @ManyToOne(type => Customer)
     customer: Customer;
 
-    @OneToMany(type => OrderItem, item => item.order)
-    items: OrderItem[];
-
-    @Column('simple-json') adjustments: Adjustment[];
-
-    @Column() totalPriceBeforeAdjustment: number;
+    @OneToMany(type => OrderLine, line => line.order)
+    lines: OrderLine[];
 
     @Column() totalPrice: number;
+
+    get adjustments() {
+        return [];
+    }
 }

+ 2 - 2
server/src/entity/order/order.graphql

@@ -4,7 +4,7 @@ type Order implements Node {
     updatedAt: DateTime!
     code: String!
     customer: Customer
-    items: [OrderItem!]!
-    adjustments: [Adjustment!]!
+    lines: [OrderLine!]!
+    adjustments: [Adjustment!]
     totalPrice: Int!
 }

+ 3 - 17
server/src/entity/adjustment-source/adjustment-source.graphql → server/src/entity/promotion/promotion.graphql

@@ -1,25 +1,13 @@
-type AdjustmentSource implements Node {
+type Promotion implements Node {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
     name: String!
-    type: AdjustmentType!
     enabled: Boolean!
     conditions: [AdjustmentOperation!]!
     actions: [AdjustmentOperation!]!
 }
 
-type Adjustment {
-    adjustmentSourceId: ID!
-    description: String!
-    amount: Int!
-}
-
-enum AdjustmentType {
-    TAX
-    PROMOTION
-    SHIPPING
-}
 
 type AdjustmentArg {
     name: String!
@@ -28,7 +16,6 @@ type AdjustmentArg {
 }
 
 type AdjustmentOperation {
-    type: AdjustmentType!
     code: String!
     args: [AdjustmentArg!]!
     description: String!
@@ -39,15 +26,14 @@ input AdjustmentOperationInput {
     arguments: [String!]!
 }
 
-input CreateAdjustmentSourceInput {
+input CreatePromotionInput {
     name: String!
-    type: AdjustmentType!
     enabled: Boolean!
     conditions: [AdjustmentOperationInput!]!
     actions: [AdjustmentOperationInput!]!
 }
 
-input UpdateAdjustmentSourceInput {
+input UpdatePromotionInput {
     id: ID!
     name: String
     enabled: Boolean

+ 32 - 0
server/src/entity/subscribers.ts

@@ -1,8 +1,40 @@
+import { EntitySubscriberInterface, EventSubscriber } from 'typeorm';
+
+import { CALCULATED_PROPERTIES } from '../common/calculated-decorator';
+
 import { ProductVariantSubscriber } from './product-variant/product-variant.subscriber';
 
+@EventSubscriber()
+export class CalculatedPropertySubscriber implements EntitySubscriberInterface {
+    /**
+     * For any entity properties decorated with @Calculated(), this subscriber transfers
+     * the getter from the entity prototype to the entity instance, so that it can be
+     * correctly enumerated and serialized in the API response.
+     */
+    afterLoad(event: any) {
+        const prototype = Object.getPrototypeOf(event);
+        if (prototype.hasOwnProperty(CALCULATED_PROPERTIES)) {
+            for (const property of prototype[CALCULATED_PROPERTIES]) {
+                const getterDescriptor = Object.getOwnPropertyDescriptor(prototype, property);
+                const getFn = getterDescriptor && getterDescriptor.get;
+                if (getFn) {
+                    const boundGetFn = getFn.bind(event);
+                    Object.defineProperties(event, {
+                        [property]: {
+                            get: () => boundGetFn(),
+                            enumerable: true,
+                        },
+                    });
+                }
+            }
+        }
+    }
+}
+
 /**
  * A map of the core TypeORM Subscribers.
  */
 export const coreSubscribersMap = {
     ProductVariantSubscriber,
+    CalculatedPropertySubscriber,
 };

+ 17 - 17
server/src/service/helpers/apply-adjustments.spec.ts → server/src/service/helpers/apply-adjustments.spec-off.ts

@@ -7,7 +7,7 @@ import {
 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 { OrderLine } from '../../entity/order-line/order-line.entity';
 import { Order } from '../../entity/order/order.entity';
 
 import { applyAdjustments, orderAdjustmentSources } from './apply-adjustments';
@@ -92,8 +92,8 @@ describe('applyAdjustments()', () => {
     it('applies a promo source to an order', () => {
         const order = new Order({
             code: 'ABC',
-            items: [
-                new OrderItem({
+            lines: [
+                new OrderLine({
                     id: 'oi1',
                     unitPrice: 300,
                     quantity: 2,
@@ -112,22 +112,22 @@ describe('applyAdjustments()', () => {
                 amount: -60,
             },
         ]);
-        expect(order.items[0].adjustments).toEqual([]);
+        expect(order.lines[0].adjustments).toEqual([]);
         expect(order.totalPrice).toBe(540);
     });
 
-    it('applies a tax source to order items', () => {
+    it('applies a tax source to order lines', () => {
         const order = new Order({
             code: 'ABC',
-            items: [
-                new OrderItem({
+            lines: [
+                new OrderLine({
                     id: 'oi1',
                     unitPrice: 300,
                     quantity: 2,
                     totalPriceBeforeAdjustment: 600,
                     taxCategoryId: standardTaxSource.id,
                 }),
-                new OrderItem({
+                new OrderLine({
                     id: 'oi2',
                     unitPrice: 450,
                     quantity: 1,
@@ -141,31 +141,31 @@ describe('applyAdjustments()', () => {
         applyAdjustments(order, [standardTaxSource, zeroTaxSource], conditions, actions);
 
         expect(order.adjustments).toEqual([]);
-        expect(order.items[0].adjustments).toEqual([
+        expect(order.lines[0].adjustments).toEqual([
             {
                 adjustmentSourceId: standardTaxSource.id,
                 description: standardTaxSource.name,
                 amount: 120,
             },
         ]);
-        expect(order.items[0].totalPrice).toBe(720);
-        expect(order.items[1].adjustments).toEqual([
+        expect(order.lines[0].totalPrice).toBe(720);
+        expect(order.lines[1].adjustments).toEqual([
             {
                 adjustmentSourceId: zeroTaxSource.id,
                 description: zeroTaxSource.name,
                 amount: 0,
             },
         ]);
-        expect(order.items[1].totalPrice).toBe(450);
+        expect(order.lines[1].totalPrice).toBe(450);
 
         expect(order.totalPrice).toBe(1170);
     });
 
-    it('evaluates promo conditions on items after tax is applied', () => {
+    it('evaluates promo conditions on lines after tax is applied', () => {
         const order = new Order({
             code: 'ABC',
-            items: [
-                new OrderItem({
+            lines: [
+                new OrderLine({
                     id: 'oi1',
                     unitPrice: 240,
                     quantity: 2,
@@ -178,14 +178,14 @@ describe('applyAdjustments()', () => {
 
         applyAdjustments(order, [promoSource1, standardTaxSource, zeroTaxSource], conditions, actions);
 
-        expect(order.items[0].adjustments).toEqual([
+        expect(order.lines[0].adjustments).toEqual([
             {
                 adjustmentSourceId: standardTaxSource.id,
                 description: standardTaxSource.name,
                 amount: 96,
             },
         ]);
-        expect(order.items[0].totalPrice).toBe(576);
+        expect(order.lines[0].totalPrice).toBe(576);
         expect(order.adjustments).toEqual([
             {
                 adjustmentSourceId: promoSource1.id,

+ 3 - 3
server/src/service/helpers/apply-adjustments.ts

@@ -31,7 +31,7 @@ export function applyAdjustments(
 
         for (const result of results) {
             if (result.orderItemId) {
-                const item = order.items.find(i => idsAreEqual(i.id, result.orderItemId));
+                const item = order.lines.find(i => idsAreEqual(i.id, result.orderItemId));
                 if (item) {
                     item.adjustments.push({
                         adjustmentSourceId: source.id,
@@ -70,7 +70,7 @@ export function orderAdjustmentSources(sources: AdjustmentSource[]): AdjustmentS
  * to be an empty array.
  */
 function initializeOrder(order: Order) {
-    for (const item of order.items) {
+    for (const item of order.lines) {
         item.totalPrice = item.totalPriceBeforeAdjustment;
         item.adjustments = [];
     }
@@ -79,7 +79,7 @@ function initializeOrder(order: Order) {
 }
 
 function getTotalPriceOfItems(order: Order): number {
-    return order.items.reduce((total, item) => total + item.totalPrice, 0);
+    return order.lines.reduce((total, item) => total + item.totalPrice, 0);
 }
 
 function getTotalAdjustmentAmount(adjustments: Adjustment[]): number {

+ 7 - 7
server/src/service/providers/adjustment-applicator.service.ts

@@ -4,27 +4,27 @@ import { Connection } from 'typeorm';
 
 import { ConfigService } from '../../config/config.service';
 import { Order } from '../../entity/order/order.entity';
-import { applyAdjustments } from '../helpers/apply-adjustments';
+// import { applyAdjustments } from '../helpers/apply-adjustments';
 
-import { AdjustmentSourceService } from './adjustment-source.service';
+import { PromotionService } from './promotion.service';
 
 @Injectable()
 export class AdjustmentApplicatorService {
     constructor(
         @InjectConnection() private connection: Connection,
         private configService: ConfigService,
-        private adjustmentSourceService: AdjustmentSourceService,
+        private adjustmentSourceService: PromotionService,
     ) {}
 
     /**
      * Applies AdjustmentSources to an order, updating the adjustment arrays of the Order and
      * its OrderItems and updating the prices based on the adjustment actions.
      */
-    async applyAdjustments(order: Order): Promise<Order> {
-        const sources = await this.adjustmentSourceService.getActiveAdjustmentSources();
+    /*async applyAdjustments(order: Order): Promise<Order> {
+        const sources = await this.adjustmentSourceService.getActivePromotions();
         const { adjustmentConditions, adjustmentActions } = this.configService;
         applyAdjustments(order, sources, adjustmentConditions, adjustmentActions);
-        await this.connection.manager.save(order.items);
+        await this.connection.manager.save(order.lines);
         return await this.connection.manager.save(order);
-    }
+    }*/
 }

+ 36 - 35
server/src/service/providers/order.service.ts

@@ -7,6 +7,7 @@ import { generatePublicId } from '../../common/generate-public-id';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { Order } from '../../entity/order/order.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { I18nError } from '../../i18n/i18n-error';
@@ -24,7 +25,7 @@ export class OrderService {
     ) {}
 
     findAll(ctx: RequestContext, options?: ListQueryOptions<Order>): Promise<PaginatedList<Order>> {
-        return buildListQuery(this.connection, Order, options, ['items', 'items.productVariant', 'customer'])
+        return buildListQuery(this.connection, Order, options, ['lines', 'lines.productVariant', 'customer'])
             .getManyAndCount()
             .then(([items, totalItems]) => {
                 return {
@@ -36,10 +37,10 @@ export class OrderService {
 
     async findOne(ctx: RequestContext, orderId: ID): Promise<Order | undefined> {
         const order = await this.connection.getRepository(Order).findOne(orderId, {
-            relations: ['items', 'items.productVariant', 'items.featuredAsset'],
+            relations: ['lines', 'lines.productVariant', 'lines.featuredAsset', 'lines.items'],
         });
         if (order) {
-            order.items.forEach(item => {
+            order.lines.forEach(item => {
                 item.productVariant = translateDeep(item.productVariant, ctx.languageCode);
             });
             return order;
@@ -49,9 +50,7 @@ export class OrderService {
     create(): Promise<Order> {
         const newOrder = new Order({
             code: generatePublicId(),
-            items: [],
-            adjustments: [],
-            totalPriceBeforeAdjustment: 0,
+            lines: [],
             totalPrice: 0,
         });
         return this.connection.getRepository(Order).save(newOrder);
@@ -66,49 +65,51 @@ export class OrderService {
         this.assertQuantityIsPositive(quantity);
         const order = await this.getOrderOrThrow(ctx, orderId);
         const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId);
-        const existingItem = order.items.find(item => idsAreEqual(item.productVariant.id, productVariantId));
+        let orderLine = order.lines.find(line => idsAreEqual(line.productVariant.id, productVariantId));
 
-        if (existingItem) {
-            return this.adjustItemQuantity(ctx, orderId, existingItem.id, existingItem.quantity + quantity);
+        if (!orderLine) {
+            const newLine = new OrderLine({
+                productVariant,
+                taxCategoryId: productVariant.taxCategory.id,
+                featuredAsset: productVariant.product.featuredAsset,
+                unitPrice: productVariant.price,
+            });
+            orderLine = await this.connection.getRepository(OrderLine).save(newLine);
+            order.lines.push(orderLine);
+            await this.connection.getRepository(Order).save(order);
         }
-        const orderItem = new OrderItem({
-            quantity,
-            productVariant,
-            taxCategoryId: productVariant.taxCategory.id,
-            featuredAsset: productVariant.product.featuredAsset,
-            unitPrice: productVariant.price,
-            unitPriceBeforeTax: 0,
-            totalPriceBeforeAdjustment: 0 * quantity,
-            totalPrice: 0 * quantity,
-            adjustments: [],
-        });
-        const newOrderItem = await this.connection.getRepository(OrderItem).save(orderItem);
-        order.items.push(newOrderItem);
-        await this.adjustmentApplicatorService.applyAdjustments(order);
-        return assertFound(this.findOne(ctx, order.id));
+        return this.adjustItemQuantity(ctx, orderId, orderLine.id, orderLine.quantity + quantity);
     }
 
     async adjustItemQuantity(
         ctx: RequestContext,
         orderId: ID,
-        orderItemId: ID,
+        orderLineId: ID,
         quantity: number,
     ): Promise<Order> {
         this.assertQuantityIsPositive(quantity);
         const order = await this.getOrderOrThrow(ctx, orderId);
-        const orderItem = this.getOrderItemOrThrow(order, orderItemId);
-        orderItem.quantity = quantity;
-        orderItem.totalPriceBeforeAdjustment = orderItem.unitPrice * orderItem.quantity;
-        await this.connection.getRepository(OrderItem).save(orderItem);
-        await this.adjustmentApplicatorService.applyAdjustments(order);
+        const orderLine = this.getOrderLineOrThrow(order, orderLineId);
+        const currentQuantity = orderLine.quantity;
+        if (currentQuantity < quantity) {
+            if (!orderLine.items) {
+                orderLine.items = [];
+            }
+            for (let i = currentQuantity; i < quantity; i++) {
+                const orderItem = await this.connection.getRepository(OrderItem).save(new OrderItem());
+                orderLine.items.push(orderItem);
+            }
+        } else if (quantity < currentQuantity) {
+            orderLine.items = orderLine.items.slice(0, quantity);
+        }
+        await this.connection.getRepository(OrderLine).save(orderLine);
         return assertFound(this.findOne(ctx, order.id));
     }
 
     async removeItemFromOrder(ctx: RequestContext, orderId: ID, orderItemId: ID): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
-        const orderItem = this.getOrderItemOrThrow(order, orderItemId);
-        order.items = order.items.filter(item => !idsAreEqual(item.id, orderItemId));
-        await this.adjustmentApplicatorService.applyAdjustments(order);
+        const orderItem = this.getOrderLineOrThrow(order, orderItemId);
+        order.lines = order.lines.filter(item => !idsAreEqual(item.id, orderItemId));
         return assertFound(this.findOne(ctx, order.id));
     }
 
@@ -134,8 +135,8 @@ export class OrderService {
         return productVariant;
     }
 
-    private getOrderItemOrThrow(order: Order, orderItemId: ID): OrderItem {
-        const orderItem = order.items.find(item => idsAreEqual(item.id, orderItemId));
+    private getOrderLineOrThrow(order: Order, orderItemId: ID): OrderLine {
+        const orderItem = order.lines.find(item => idsAreEqual(item.id, orderItemId));
         if (!orderItem) {
             throw new I18nError(`error.order-does-not-contain-item-with-id`, { id: orderItemId });
         }

+ 3 - 6
server/src/service/providers/product-variant.service.ts

@@ -20,7 +20,6 @@ import { translateDeep } from '../helpers/translate-entity';
 import { TranslationUpdaterService } from '../helpers/translation-updater.service';
 import { updateTranslatable } from '../helpers/update-translatable';
 
-import { AdjustmentSourceService } from './adjustment-source.service';
 import { TaxCategoryService } from './tax-category.service';
 
 @Injectable()
@@ -29,11 +28,10 @@ export class ProductVariantService {
         @InjectConnection() private connection: Connection,
         private taxCategoryService: TaxCategoryService,
         private translationUpdaterService: TranslationUpdaterService,
-        private adjustmentSourceService: AdjustmentSourceService,
     ) {}
 
     findOne(ctx: RequestContext, productVariantId: ID): Promise<Translated<ProductVariant> | undefined> {
-        const relations = ['product', 'product.featuredAsset'];
+        const relations = ['product', 'product.featuredAsset', 'taxCategory'];
         return this.connection
             .getRepository(ProductVariant)
             .findOne(productVariantId, { relations })
@@ -112,9 +110,8 @@ export class ProductVariantService {
             ? generateAllCombinations(product.optionGroups.map(g => g.options))
             : [[]];
 
-        const taxCategoryId =
-            defaultTaxCategoryId ||
-            (await this.adjustmentSourceService.getDefaultTaxCategory()).id.toString();
+        // TODO: how to handle default tax category?
+        const taxCategoryId = defaultTaxCategoryId || '1';
 
         const variants: ProductVariant[] = [];
         for (const options of optionCombinations) {

+ 24 - 45
server/src/service/providers/adjustment-source.service.ts → server/src/service/providers/promotion.service.ts

@@ -3,9 +3,8 @@ import { InjectConnection } from '@nestjs/typeorm';
 import {
     AdjustmentOperation,
     AdjustmentOperationInput,
-    AdjustmentType,
-    CreateAdjustmentSourceInput,
-    UpdateAdjustmentSourceInput,
+    CreatePromotionInput,
+    UpdatePromotionInput,
 } from 'shared/generated-types';
 import { omit } from 'shared/omit';
 import { ID, PaginatedList } from 'shared/shared-types';
@@ -19,7 +18,7 @@ import {
     AdjustmentConditionDefinition,
 } from '../../config/adjustment/adjustment-types';
 import { ConfigService } from '../../config/config.service';
-import { AdjustmentSource } from '../../entity/adjustment-source/adjustment-source.entity';
+import { Promotion } from '../../entity/promotion/promotion.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { buildListQuery } from '../helpers/build-list-query';
 import { patchEntity } from '../helpers/patch-entity';
@@ -27,7 +26,7 @@ import { patchEntity } from '../helpers/patch-entity';
 import { ChannelService } from './channel.service';
 
 @Injectable()
-export class AdjustmentSourceService {
+export class PromotionService {
     availableConditions: AdjustmentConditionDefinition[] = [];
     availableActions: AdjustmentActionDefinition[] = [];
     /**
@@ -35,7 +34,7 @@ export class AdjustmentSourceService {
      * every time an order is changed, which will happen often. Caching them means
      * a DB call is not required newly each time.
      */
-    private activeSources: AdjustmentSource[] = [];
+    private activePromotions: Promotion[] = [];
 
     constructor(
         @InjectConnection() private connection: Connection,
@@ -46,8 +45,8 @@ export class AdjustmentSourceService {
         this.availableActions = this.configService.adjustmentActions;
     }
 
-    findAll(options?: ListQueryOptions<AdjustmentSource>): Promise<PaginatedList<AdjustmentSource>> {
-        return buildListQuery(this.connection, AdjustmentSource, options)
+    findAll(options?: ListQueryOptions<Promotion>): Promise<PaginatedList<Promotion>> {
+        return buildListQuery(this.connection, Promotion, options)
             .getManyAndCount()
             .then(([items, totalItems]) => ({
                 items,
@@ -55,8 +54,8 @@ export class AdjustmentSourceService {
             }));
     }
 
-    async findOne(adjustmentSourceId: ID): Promise<AdjustmentSource | undefined> {
-        return this.connection.manager.findOne(AdjustmentSource, adjustmentSourceId, {
+    async findOne(adjustmentSourceId: ID): Promise<Promotion | undefined> {
+        return this.connection.manager.findOne(Promotion, adjustmentSourceId, {
             relations: [],
         });
     }
@@ -64,60 +63,41 @@ export class AdjustmentSourceService {
     /**
      * Returns all available AdjustmentOperations.
      */
-    getAdjustmentOperations(
-        type: AdjustmentType,
-    ): {
+    getAdjustmentOperations(): {
         conditions: AdjustmentConditionDefinition[];
         actions: AdjustmentActionDefinition[];
     } {
         return {
-            conditions: this.availableConditions.filter(o => o.type === type),
-            actions: this.availableActions.filter(o => o.type === type),
+            conditions: this.availableConditions,
+            actions: this.availableActions,
         };
     }
 
     /**
      * Returns all active AdjustmentSources.
      */
-    async getActiveAdjustmentSources(): Promise<AdjustmentSource[]> {
-        if (!this.activeSources.length) {
-            await this.updateActiveSources();
+    async getActivePromotions(): Promise<Promotion[]> {
+        if (!this.activePromotions.length) {
+            await this.updatePromotions();
         }
-        return this.activeSources;
+        return this.activePromotions;
     }
 
-    /**
-     * Returns the default tax category.
-     * TODO: currently just returns the first one. There should be a "default" flag.
-     */
-    async getDefaultTaxCategory(): Promise<AdjustmentSource> {
-        const sources = await this.getActiveAdjustmentSources();
-        const taxCategories = sources.filter(s => s.type === AdjustmentType.TAX);
-        return taxCategories[0];
-    }
-
-    async createAdjustmentSource(
-        ctx: RequestContext,
-        input: CreateAdjustmentSourceInput,
-    ): Promise<AdjustmentSource> {
-        const adjustmentSource = new AdjustmentSource({
+    async createPromotion(ctx: RequestContext, input: CreatePromotionInput): Promise<Promotion> {
+        const adjustmentSource = new Promotion({
             name: input.name,
-            type: input.type,
             enabled: input.enabled,
             conditions: input.conditions.map(c => this.parseOperationArgs('condition', c)),
             actions: input.actions.map(a => this.parseOperationArgs('action', a)),
         });
         this.channelService.assignToChannels(adjustmentSource, ctx);
         const newAdjustmentSource = await this.connection.manager.save(adjustmentSource);
-        await this.updateActiveSources();
+        await this.updatePromotions();
         return assertFound(this.findOne(newAdjustmentSource.id));
     }
 
-    async updateAdjustmentSource(
-        ctx: RequestContext,
-        input: UpdateAdjustmentSourceInput,
-    ): Promise<AdjustmentSource> {
-        const adjustmentSource = await this.connection.getRepository(AdjustmentSource).findOne(input.id);
+    async updatePromotion(ctx: RequestContext, input: UpdatePromotionInput): Promise<Promotion> {
+        const adjustmentSource = await this.connection.getRepository(Promotion).findOne(input.id);
         if (!adjustmentSource) {
             throw new I18nError(`error.entity-with-id-not-found`, {
                 entityName: 'AdjustmentSource',
@@ -134,7 +114,7 @@ export class AdjustmentSourceService {
             updatedAdjustmentSource.actions = input.actions.map(a => this.parseOperationArgs('action', a));
         }
         await this.connection.manager.save(updatedAdjustmentSource);
-        await this.updateActiveSources();
+        await this.updatePromotions();
         return assertFound(this.findOne(updatedAdjustmentSource.id));
     }
 
@@ -148,7 +128,6 @@ export class AdjustmentSourceService {
         const match = this.getAdjustmentOperationByCode(type, input.code);
         const output: AdjustmentOperation = {
             code: input.code,
-            type: match.type,
             description: match.description,
             args: input.arguments.map((inputArg, i) => {
                 return {
@@ -174,8 +153,8 @@ export class AdjustmentSourceService {
     /**
      * Update the activeSources cache.
      */
-    private async updateActiveSources() {
-        this.activeSources = await this.connection.getRepository(AdjustmentSource).find({
+    private async updatePromotions() {
+        this.activePromotions = await this.connection.getRepository(Promotion).find({
             where: { enabled: true },
         });
     }

+ 2 - 2
server/src/service/service.module.ts

@@ -6,7 +6,6 @@ import { getConfig } from '../config/vendure-config';
 
 import { TranslationUpdaterService } from './helpers/translation-updater.service';
 import { AdjustmentApplicatorService } from './providers/adjustment-applicator.service';
-import { AdjustmentSourceService } from './providers/adjustment-source.service';
 import { AdministratorService } from './providers/administrator.service';
 import { AssetService } from './providers/asset.service';
 import { AuthService } from './providers/auth.service';
@@ -22,12 +21,13 @@ import { ProductOptionGroupService } from './providers/product-option-group.serv
 import { ProductOptionService } from './providers/product-option.service';
 import { ProductVariantService } from './providers/product-variant.service';
 import { ProductService } from './providers/product.service';
+import { PromotionService } from './providers/promotion.service';
 import { RoleService } from './providers/role.service';
 import { TaxCategoryService } from './providers/tax-category.service';
 import { ZoneService } from './providers/zone.service';
 
 const exportedProviders = [
-    AdjustmentSourceService,
+    PromotionService,
     AdministratorService,
     AssetService,
     AuthService,

File diff suppressed because it is too large
+ 343 - 383
shared/generated-types.ts


Some files were not shown because too many files changed in this diff