Order Service page

Writing the Order Service

Overview

In this part, we will:

  • Create a new order service
  • Write interfaces to describe orders and items in orders
  • Add data methods to our service
  • Import our new order service to our component & create an order
  • Show completed order in UI

Problem 1: Create Order Service and Export Items and Order interfaces

We need to create a new service to handle creating and updating orders. We'll need two interfaces - one to describe the order, one to describe items in the order.

P1: Technical Requirements

Create a new service order in the order directory, and write and export Order and Item interfaces representing these objects in the new service:

let myorder = {
  _id: 'a123123bdd',
  name: 'Jennifer',
  address: '123 Main st',
  phone: '867-5309'
  status: 'new',
  items: [
    {
      name: 'tacos',
      price: 6.99
    }
  ]
}

P1: What you Need to Know

  • How to create services (you learned this in previous sections! ✔️)

  • How to write interfaces (you learned this in previous sections! ✔️)

      ng g service order/order
    

P1: Solution

✏️ Update src/app/order/order.service.ts

import { Injectable } from '@angular/core';

export interface Item {
  name: string;
  price: number;
}

export interface Order {
  _id: string;
  name: string;
  address: string;
  phone: string;
  status: string;
  items: Array<Item>;
}

@Injectable({
  providedIn: 'root'
})
export class OrderService {

  constructor() { }

}

Problem 2: Finish the Order Service

With our order service we'll want to be able to create new orders, updating existing orders, delete orders, and view all orders.

P2: Setup

✏️ Paste the following into src/app/order/order.service.spec.ts:

import { TestBed } from '@angular/core/testing';

import { OrderService } from './order.service';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpRequest } from '@angular/common/http';

describe('OrderService', () => {
  let httpMock : HttpTestingController;
  let orderService: OrderService;
  beforeEach(() => TestBed.configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [
      OrderService
    ]
  }));

  beforeEach(() => {
    httpMock = TestBed.get(HttpTestingController);
    orderService = TestBed.get(OrderService);
  })

  it('should make a get request to get orders', () => {
    const mockOrder = {
      data: [
        {_id: 'adsfsdf',
        name: 'Jennifer',
        address: '123 main',
        phone: '555-555-5555',
        status: 'new',
        items: [
          {
            name: 'nummy fries',
            price: 2.56
          }
        ]}
      ]
    };

    orderService.getOrders().subscribe((orders) => {
      expect(orders).toEqual(mockOrder);
    });

    let url = 'http://localhost:7070/orders';
    const req = httpMock.expectOne(url);

    expect(req.request.method).toEqual('GET');
    req.flush(mockOrder);

    httpMock.verify();
  });

  it('should make a get request to create an order', () => {
    const mockOrder = {
      data: [
        {_id: 'adsfsdf',
        name: 'Jennifer',
        address: '123 main',
        phone: '555-555-5555',
        status: 'old',
        items: [
          {
            name: 'nummy fries',
            price: 2.56
          }
        ]}
      ]
    };

    const createdOrder = {
      _id: 'adsfsdf',
      name: 'Jennifer',
      address: '123 main',
      phone: '555-555-5555',
      status: 'new',
      items: [
        {
          name: 'nummy fries',
          price: 2.56
        }
      ]}

    orderService.createOrder(mockOrder.data[0]).subscribe((orders) => {
      expect(orders).toEqual(mockOrder);
    });

    let url = 'http://localhost:7070/orders';
    httpMock.expectOne((request: HttpRequest<any>) => {
      console.log(request.body);
      return request.method == 'POST'
        && request.url == url
        && JSON.stringify(request.body) == JSON.stringify(createdOrder);
    }).flush(mockOrder);

    httpMock.verify();
  });

  it('should make a put request to update an order', () => {
    const mockOrder = {
      data: [
        {_id: 'adsfsdf',
        name: 'Jennifer',
        address: '123 main',
        phone: '555-555-5555',
        status: 'old',
        items: [
          {
            name: 'nummy fries',
            price: 2.56
          }
        ]}
      ]
    };

    const createdOrder = {
      _id: 'adsfsdf',
      name: 'Jennifer',
      address: '123 main',
      phone: '555-555-5555',
      status: 'delivered',
      items: [
        {
          name: 'nummy fries',
          price: 2.56
        }
      ]};

    orderService.updateOrder(mockOrder.data[0], 'delivered').subscribe((orders) => {
      expect(orders).toEqual(mockOrder);
    });

    let url = 'http://localhost:7070/orders/adsfsdf';
    httpMock.expectOne((request: HttpRequest<any>) => {
      console.log(request.body);
      return request.method == 'PUT'
        && request.url == url
        && JSON.stringify(request.body) == JSON.stringify(createdOrder);
    }).flush(mockOrder);

    httpMock.verify();
  });

  it('should make a delete request to delete an order', () => {
    const mockOrder = {
      data: [
        {_id: 'adsfsdf',
        name: 'Jennifer',
        address: '123 main',
        phone: '555-555-5555',
        status: 'old',
        items: [
          {
            name: 'nummy fries',
            price: 2.56
          }
        ]}
      ]
    };

    orderService.deleteOrder('adsfsdf').subscribe((orders) => {
      expect(orders).toEqual(mockOrder);
    });

    let url = 'http://localhost:7070/orders/adsfsdf';
    const req = httpMock.expectOne(url);

    expect(req.request.method).toEqual('DELETE');
    req.flush(mockOrder);

    httpMock.verify();
  });
});

Now, try to understand what this test is doing and implement the remainder of order.service.ts to get the tests to pass.

✏️ Run your tests with:

ng test

P2: What You Need to Know

  • You will need to look at the test code to determine the method signatures on OrderService.
  • You will need to make sure HttpClient is imported and added as a property in the OrderService constructor.

P2: Solution

✏️ Update src/app/order/order.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';

export interface Item {
  name: string;
  price: number;
}

export interface Order {
  _id: string;
  name: string;
  address: string;
  phone: string;
  status: string;
  items: Array<Item>;
}

@Injectable({
  providedIn: 'root'
})
export class OrderService {

  constructor(private httpClient: HttpClient) { }

  getOrders() {
    return this.httpClient.get(environment.apiUrl + '/orders');
  }

  createOrder(order: Order) {
    let orderData = Object.assign({}, order);
    orderData.status = 'new';
    return this.httpClient.post(environment.apiUrl + '/orders', orderData)
  }

  updateOrder(order: Order, action: string) {
    let orderData = Object.assign({}, order);
    orderData.status = action;
    return this.httpClient.put(environment.apiUrl + '/orders/' + orderData._id, orderData);
  }

  deleteOrder(id: string) {
    return this.httpClient.delete(environment.apiUrl + '/orders/' + id);
  }
}

Problem 3: Use the OrderService in the OrderComponent to Create an Order

P3: Technical Requirements

For this problem, we will:

  • Create an order when the user submits a service.
  • Disable the button while the order is being processed.
  • Show the completed order to the user.
  • Let the user start a new order.

How we will solve this:

  1. We will import the order service, and save it as orderService in the OrderComponent's constructor.
  2. Call orderService's createOrder with the orderForm's values.
  3. While the order is being created orderProcessing should be true.
  4. Once complete, orderComplete should be set to true and set back to false when .startNewOrder() is called.
  5. We will save the completed order in completedOrder.

P3: Setup

Before starting:

1. ✏️ Update src/app/order/order.component.html to show the completed order:

<div class="order-form">
  <ng-container *ngIf="orderComplete; else showOrderForm">
    <h3>Thanks for your order {{completedOrder.name}}!</h3>
    <div><label class="control-label">
      Confirmation Number: {{completedOrder._id}}</label>
    </div>

    <h4>Items ordered:</h4>
    <ul class="list-group panel">
      <li class="list-group-item" *ngFor="let item of completedOrder.items">
        <label>
          {{item.name}} <span class="badge">${{item.price}}</span>
        </label>
      </li>

      <li class="list-group-item">
        <label>
          Total <span class="badge">${{orderTotal}}</span>
        </label>
      </li>
    </ul>

    <div><label class="control-label">
      Phone: {{completedOrder.phone}}
    </label></div>
    <div><label class="control-label">
      Address: {{completedOrder.address}}
      </label></div>
    <p>
      <button (click)="startNewOrder()">
        Place another order
      </button>
    </p>
  </ng-container>
  <ng-template #showOrderForm>
    <h2>Order here</h2>
    <form *ngIf="orderForm" [formGroup]="orderForm" (ngSubmit)="onSubmit()">
      <tabset>
        <tab heading="Lunch Menu">
          <ul class="list-group">
            <pmo-menu-items [items]="restaurant.menu.lunch" formControlName="items"></pmo-menu-items>
          </ul>
        </tab>
        <tab heading="Dinner menu">
          <ul class="list-group">
            <pmo-menu-items [items]="restaurant.menu.dinner" formControlName="items"></pmo-menu-items>
          </ul>
        </tab>
      </tabset>
      <div class="form-group">
        <label class="control-label">Name:</label>
        <input name="name" type="text" class="form-control" formControlName="name">
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" class="form-control" formControlName="address">
        <p class="help-text">Please enter your address.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Phone:</label>
        <input name="phone" type="text" class="form-control" formControlName="phone">
        <p class="help-text">Please enter your phone number.</p>
      </div>
      <div class="submit">
        <h4>Total: ${{orderTotal}}</h4>
          <div class="loading" *ngIf="orderProcessing"></div>
          <button type="submit" [disabled]="!orderForm.valid || orderProcessing" class="btn">
            Place My Order!
          </button>
      </div>
    </form>
  </ng-template>
</div>

2. ✏️ Update src/app/order/order.component.ts to have a onSubmit method and a startNewOrder that will start a new order.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, FormArray, AbstractControl } from '@angular/forms';

import { RestaurantService } from '../restaurant/restaurant.service';
import { Restaurant } from '../restaurant/restaurant';
import { Subscription } from 'rxjs';


function minLengthArray(min: number) {
  return (c: AbstractControl): {[key: string]: any} => {
      if (c.value.length >= min)
          return null;
      return { 'minLengthArray': {valid: false }};
  }
}

@Component({
  selector: 'pmo-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.less']
})
export class OrderComponent implements OnInit, OnDestroy {
  orderForm: FormGroup;
  restaurant: Restaurant;
  isLoading: boolean = true;
  items: FormArray;
  orderTotal: number = 0.0;
  completedOrder: any;
  orderComplete: boolean = false;
  orderProcessing: boolean = false;
  private subscription: Subscription;

  constructor(
    private route: ActivatedRoute, 
    private restaurantService: RestaurantService,
    private formBuilder: FormBuilder 
  ) { 
  }

  ngOnInit() {
    const slug = this.route.snapshot.paramMap.get('slug');

    this.restaurantService.getRestaurant(slug).subscribe((data:Restaurant) => {
      this.restaurant = data;
      this.isLoading = false;      
      this.createOrderForm();
    })
  }

  ngOnDestroy(): void {
    if(this.subscription) {
      this.subscription.unsubscribe();
    }
  }

  createOrderForm() {
    this.orderForm = this.formBuilder.group({
      restaurant: [this.restaurant._id],
      name: [null],
      address:  [null],
      phone: [null],
      items: [[], minLengthArray(1)]
    });
    this.onChanges();
  }

  onChanges() {
    this.subscription = this.orderForm.get('items').valueChanges.subscribe(val => {
      let total = 0.0;
      val.forEach((item: any) => {
        total += item.price;
      });
      this.orderTotal = Math.round(total * 100) / 100;
    });
  }

  onSubmit() {
    this.orderProcessing = true;
    //call createOrder here
  }

  startNewOrder() {
    this.orderComplete = false;
    this.orderTotal = 0.0;
    this.createOrderForm();
  }

}

P3: How to Verify Your Solution is Correct

If you've implemented everything correctly, you should now be able to create an order from the UI and see a record of your completed order once it's created.

✏️ Update the menu-items spec file src/app/order/order.component.spec.ts to be:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs'; 

import { OrderComponent } from './order.component';
import { ReactiveFormsModule } from '@angular/forms';
import { RestaurantService } from '../restaurant/restaurant.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MenuItemsComponent } from './menu-items/menu-items.component';
import { OrderService } from './order.service';

class MockRestaurantService {
  getRestaurant(slug:string) {
    return of({
        "name": "Poutine Palace",
        "slug": "poutine-palace",
        "images": {
          "thumbnail": "node_modules/place-my-order-assets/images/4-thumbnail.jpg",
          "owner": "node_modules/place-my-order-assets/images/3-owner.jpg",
          "banner": "node_modules/place-my-order-assets/images/2-banner.jpg"
        },
        "menu": {
          "lunch": [
            {
              "name": "Crab Pancakes with Sorrel Syrup",
              "price": 35.99
            },
            {
              "name": "Steamed Mussels",
              "price": 21.99
            },
            {
              "name": "Spinach Fennel Watercress Ravioli",
              "price": 35.99
            }
          ],
          "dinner": [
            {
              "name": "Gunthorp Chicken",
              "price": 21.99
            },
            {
              "name": "Herring in Lavender Dill Reduction",
              "price": 45.99
            },
            {
              "name": "Chicken with Tomato Carrot Chutney Sauce",
              "price": 45.99
            }
          ]
        },
        "address": {
          "street": "230 W Kinzie Street",
          "city": "Green Bay",
          "state": "WI",
          "zip": "53205"
        },
        "_id": "3ZOZyTY1LH26LnVw"
      })
  }
}

class MockOrderService {
  createOrder(order) {
    return of({
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Jennifer Hungry",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "preparing",
      "_id": "0awcHyo3iD6CpvhX",
    })
  }
}

const MockActivatedRoute = {
  snapshot: {
    paramMap: {
      get() {
        return 'poutine-palace'
      }
    }
  }
}

describe('OrderComponent', () => {
  let component: OrderComponent;
  let fixture: ComponentFixture<OrderComponent>;
  let injectedOrderService;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ OrderComponent, MenuItemsComponent ], 
      imports: [  ReactiveFormsModule, RouterTestingModule ],
      providers: [{
        provide: RestaurantService,
        useClass: MockRestaurantService
      }, {
        provide: ActivatedRoute,
        useValue: MockActivatedRoute
      },
      {
        provide: OrderService,
        useClass: MockOrderService
      }],
      schemas: [ NO_ERRORS_SCHEMA ]
    })
    .compileComponents();
    injectedOrderService = TestBed.get(OrderService);
  }));
  

  beforeEach(() => {
    fixture = TestBed.createComponent(OrderComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should get a restaurant based on route slug', () => {
    let mockRestaurant = {
      "name": "Poutine Palace",
      "slug": "poutine-palace",
      "images": {
        "thumbnail": "node_modules/place-my-order-assets/images/4-thumbnail.jpg",
        "owner": "node_modules/place-my-order-assets/images/3-owner.jpg",
        "banner": "node_modules/place-my-order-assets/images/2-banner.jpg"
      },
      "menu": {
        "lunch": [
          {
            "name": "Crab Pancakes with Sorrel Syrup",
            "price": 35.99
          },
          {
            "name": "Steamed Mussels",
            "price": 21.99
          },
          {
            "name": "Spinach Fennel Watercress Ravioli",
            "price": 35.99
          }
        ],
        "dinner": [
          {
            "name": "Gunthorp Chicken",
            "price": 21.99
          },
          {
            "name": "Herring in Lavender Dill Reduction",
            "price": 45.99
          },
          {
            "name": "Chicken with Tomato Carrot Chutney Sauce",
            "price": 45.99
          }
        ]
      },
      "address": {
        "street": "230 W Kinzie Street",
        "city": "Green Bay",
        "state": "WI",
        "zip": "53205"
      },
      "_id": "3ZOZyTY1LH26LnVw"
    }
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    expect(fixture.componentInstance.restaurant).toEqual(mockRestaurant)
  });

  it('should have an orderForm formGroup', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    expect(fixture.componentInstance.orderForm.controls.restaurant).toBeTruthy();
    expect(fixture.componentInstance.orderForm.controls.address).toBeTruthy();
    expect(fixture.componentInstance.orderForm.controls.phone).toBeTruthy();
    expect(fixture.componentInstance.orderForm.controls.items).toBeTruthy();
  });

  it('should have a validator on items formControl', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let itemFormControl = fixture.componentInstance.orderForm.controls.items;
    expect(itemFormControl.valid).toEqual(false);
  });

  it('should update items FormControl when setUpdatesItems is called', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let childInput = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[0];
    let formItems = fixture.componentInstance.orderForm.get('items');
    childInput.click();
    fixture.detectChanges();
    expect(formItems.value).toEqual([{"name": "Crab Pancakes with Sorrel Syrup","price": 35.99}])
  });

  it('should update the order total when the items FormControl value changes', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let childInput1 = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[0];
    let childInput2 = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[1];
    childInput1.click();
    childInput2.click();
    fixture.detectChanges();
    let orderText = compiled.querySelector('.submit h4');
    expect(orderText.textContent).toEqual('Total: $57.98')
  });

  it('should call the orderService createOrder on form submit with form values', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let createOrderSpy = spyOn(injectedOrderService, 'createOrder').and.callThrough();
    let expectedOrderValue = {
      restaurant: '12345',
      name: 'Jennifer Hungry',
      address:  '123 Main St',
      phone: "555-555-5555",
      items: [{name: "Onion fries", price: 15.99}, {name: "Roasted Salmon", price: 23.99}]
    };
    let compiled = fixture.debugElement.nativeElement;
    fixture.componentInstance.orderForm.setValue(expectedOrderValue);
    fixture.detectChanges();
    compiled.querySelector('button[type="submit"]').click();
    expect(createOrderSpy).toHaveBeenCalledWith(expectedOrderValue);
  });

  it('should show completed order when order is complete', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let expectedOrderValue = {
      restaurant: '12345',
      name: 'Jennifer Hungry',
      address:  '123 Main St',
      phone: "555-555-5555",
      items: [{name: "Onion fries", price: 15.99}, {name: "Roasted Salmon", price: 23.99}]
    };
    let compiled = fixture.debugElement.nativeElement;
    fixture.componentInstance.orderForm.setValue(expectedOrderValue);
    fixture.detectChanges();
    compiled.querySelector('button[type="submit"]').click();
    fixture.detectChanges();
    let displayedOrder = compiled.querySelector('h3');
    expect(displayedOrder.textContent).toEqual('Thanks for your order Jennifer Hungry!');
  });

  it('should clear the form values when create new order is clicked', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let expectedOrderValue = {
      restaurant: '12345',
      name: 'Jennifer Hungry',
      address:  '123 Main St',
      phone: "555-555-5555",
      items: [{name: "Onion fries", price: 15.99}, {name: "Roasted Salmon", price: 23.99}]
    };
    let compiled = fixture.debugElement.nativeElement;
    fixture.componentInstance.orderForm.setValue(expectedOrderValue);
    fixture.detectChanges();
    compiled.querySelector('button[type="submit"]').click();
    fixture.detectChanges();
    compiled.querySelector('button:nth-child(1)').click();
    let emptyform = {
      restaurant: '3ZOZyTY1LH26LnVw',
      name: null,
      address: null,
      phone: null,
      items: []
    }
    expect(fixture.componentInstance.orderForm.value).toEqual(emptyform);
  });

});

If you've implemented the solution correctly, when you run npm run test all tests will pass!

P3: What You Need to Know

  • How to import a service
  • How to call a method on a service and get the result
  • How to show/hide content using *ngIf

P3: Solution

✏️ Update src/app/order/order.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, FormArray, AbstractControl } from '@angular/forms';

import { RestaurantService } from '../restaurant/restaurant.service';
import { Restaurant } from '../restaurant/restaurant';
import { OrderService, Order, Item } from './order.service';
import { Subscription } from 'rxjs';


function minLengthArray(min: number) {
  return (c: AbstractControl): {[key: string]: any} => {
      if (c.value.length >= min)
          return null;
      return { 'minLengthArray': {valid: false }};
  }
}

@Component({
  selector: 'pmo-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.less']
})
export class OrderComponent implements OnInit, OnDestroy {
  orderForm: FormGroup;
  restaurant: Restaurant;
  isLoading: boolean = true;
  items: FormArray;
  orderTotal: number = 0.0;
  completedOrder: Order;
  orderComplete: boolean = false;
  orderProcessing: boolean = false;
  private subscription: Subscription;

  constructor(
    private route: ActivatedRoute, 
    private restaurantService: RestaurantService,
    private orderService: OrderService,
    private formBuilder: FormBuilder 
  ) { 
  }

  ngOnInit() {
    const slug = this.route.snapshot.paramMap.get('slug');

    this.restaurantService.getRestaurant(slug).subscribe((data:Restaurant) => {
      this.restaurant = data;
      this.isLoading = false;      
      this.createOrderForm();
    })
  }

  ngOnDestroy(): void {
    if(this.subscription) {
      this.subscription.unsubscribe();
    }
  }

  createOrderForm() {
    this.orderForm = this.formBuilder.group({
      restaurant: [this.restaurant._id],
      name: [null],
      address:  [null],
      phone: [null],
      items: [[], minLengthArray(1)]
    });
    this.onChanges();
  }

  onChanges() {
    this.subscription = this.orderForm.get('items').valueChanges.subscribe(val => {
      let total = 0.0;
      val.forEach((item: Item) => {
        total += item.price;
      });
      this.orderTotal = Math.round(total * 100) / 100;
    });
  }

  onSubmit() {
    this.orderProcessing = true;
    this.orderService.createOrder(this.orderForm.value).subscribe((res: Order) => {
      this.completedOrder = res;
      this.orderComplete = true;
      this.orderProcessing = false;
    });
  }

  startNewOrder() {
    this.orderComplete = false;
    this.orderTotal = 0.0;
    this.createOrderForm();
  }

}