App Flow Demo - nuthanc/microservice GitHub Wiki

App Demo Flow

  • In the index page, all the tickets are listed
* This is from Next's index.js page
* tickets are fetched in the getInitialProps(request to /api/tickets)
* getInitialProps will be called automatically while Next is trying to render our App on the server
* In the LandingPage component, we iterate over the tickets and render each ticket's title, price and link
  • In the Header, we have Sell Tickets, My Orders and Sign Out
* To have a common header in all the pages, we use Header in _app.js and all the other components are present after the Header
* If the user isn't logged in, the Header shows only Sign Up(/auth/signup in client) and Sign In(/auth/signin in client)
* More details in **Auth-flow**
  • When the User clicks on Sell Tickets, it redirects to /tickets/new where Title and Price should be provided
  • After a Ticket is created, it redirects to the index page
* Behind the scenes, the ticket is **saved in db**
  * While building, it gets userId from req.currentUser.id 
  * While storing, it gets version property added automatically by mongoose's update-if-current plugin
* After the ticket is saved, **TicketCreatedPublisher** publishes TicketCreatedEvent with properties id(ticket), title, price, userId and version
* The listener for this event is **Orders service**(using **TicketCreatedListener** which starts listening in index.ts)
* The **orders TicketCreatedListener's** onMessage pulls out id, title and price of the ticket and stores it in db
  * It doesn't pull userId because it doesn't matter who created the ticket, but who ordered the ticket matters
  * Also version is added automatically by the plugin
  * While storing, it adds version and isReserved properties
* After this, it sends an ack message(Ack to NATS that event received)
  • Other Users can try to buy the ticket by clicking on View for the corresponding ticket in the index page
  • After clicking on View, it redirects to tickets/[ticketId], where you can purchase the ticket
  • By clicking on Purchase, the User is directed to /orders/[orderId]
* Behind the scenes, clicking on Purchase makes a Post request to /api/orders with body as ticketId
* The Orders service fetches the ticket from the db using the ticketId
* It checks whether the ticket is reserved or not. If it's reserved, then throws a BadRequestError
* If it's not reserved, an expiration date is calculated for this Order and is assigned to expiresAt
* Order is built with userId, status, expiresAt and the clicked ticket
* Then while storing, version is automatically added and is **saved in db**
* After saving in the db, **OrderCreatedPublisher** publishes OrderCreatedEvent with properties id(order),status, userId, expiresAt, ticket(object containing id and price) and version
* The listeners for this event are **Tickets, Payments and Expiration services**(using **OrderCreatedListener** which starts listening in index.ts)
  * The **tickets OrderCreatedListener's** onMessage pulls out id of ticket
    * Using that id, the ticket is fetched from the db
    * If there is no ticket, an Error is thrown
    * Else the ticket sets its orderId property(This marks the ticket as being reserved) and is saved to the db
    * Then **TicketUpdatedPublisher** publishes TicketUpdatedEvent with properties id(ticket), title, price, userId, orderId and version
    * The listener for this event is **Orders service**(using **TicketUpdatedListener** which starts listening in index.ts) 
    * The **orders TicketUpdatedListener's** onMessage provides data to ticket model's(ticket model in orders service) findByEvent
      * The findByEvent method pulls out id and version and assigns to event object
      * Then using this id(event.id) and version(event.version - 1), the ticket is fetched from the db and returned
    * If ticket is not found, an Error is thrown
    * Else the title and price is pulled out of the data and is set on the ticket
    * The ticket is saved in the db and ack message is sent
  * The **payments OrderCreatedListener's** onMessage pulls out id of order, price, status, userId and version and builds an order
    * The order is **saved in db** and ack message is sent
  * The **expiration OrderCreatedListener's** onMessage pulls expiresAt property and calculates the delay
    * Then the orderId with the delay is added to the expirationQueue
    * Bull's usecase is for jobs like this(Workflow: Bull -> Job -> Redis -> Worker Server)
    * Redis is fantastics for jobs(things scheduled for some point in time)
    * Flow:
      * 'order:created' -> Expiration Service(expirationQueue with bull) -> Code to enqueue a job -> Redis Server(List of jobs with type 'order:expiration') -> Job orderId -> Expiration Service(expirationQueue) -> Code to process a job -> expiration:complete(for orderId)
    * The orderId will be stored in the Redis server
    * After the expiration is complete, the expirationQueue is processed
    * While processing, **ExpirationCompletePublisher** publishes ExpirationCompletedEvent with property orderId
      * The listener for this event is **Orders service**(using **ExpirationCompleteListener** which starts listening in index.ts) 
      * The **orders ExpirationCompleteListener's** onMessage pulls out orderId from data
      * Using orderId, the order is fetched from db and the ticket is populated
      * If the order is not found, an Error is thrown
      * If the order status is Complete, then ack message is sent
      * Else the order sets its status as Cancelled and saves it to db
      * Then **OrderCancelledPublisher** publishes OrderCancelledEvent with properties id(order), version(order), ticket(object containing id) after which ack message is sent
        * The listeners for this event are **Tickets and Payments services**(using **OrderCancelledListener** which starts listening in index.ts)
        * The **tickets OrderCancelledListener's** onMessage pulls out id(data.ticket.id)
          * Using the ticket id, the ticket is fetched
          * If there is no ticket, an Error is thrown
          * Else, the ticket sets orderId property as undefined
          * The ticket is then saved to the db
          * Then the **TicketUpdatedPublisher** publishes TicketUpdatedEvent with properties id(ticket), price, title, userId, orderId and version(ticket)
          * The listener for this event is **Orders service**(using **TicketUpdatedListener** which starts listening in index.ts)
          * The **orders TicketUpdatedListener's** onMessage provides data to ticket model's(ticket model in orders service) findByEvent
            * The findByEvent method pulls out id and version and assigns to event object
            * Then using this id(event.id) and version(event.version - 1), the ticket is fetched from the db and returned
          * If ticket is not found, an Error is thrown
          * Else the title and price is pulled out of the data and is set on the ticket
          * The ticket is saved in the db and ack message is sent
        * The **payments OrderCancelledListener's** onMessage pulls out id(order) and version(data.version - 1)
        * Using this, order is fetched from db
        * If order is not found, an Error is thrown
        * Else order sets is status as Cancelled and saves it to db
        * Then ack message is sent
  • The timer will be running in the background after being to /orders/[orderId]
* In OrderShow's getInitialProps, order details are fetched from the orderId
* Then in useEffect timeLeft(state) is set(using setTimeLeft) by substracting expiresAt Date to current Date
* Then setInterval is run every second for findTimeLeft function
* Whenever we return a function from useEffect, that function will be invoked when we are gonna navigate away from the Component or rerendered(dependency in the array for rerendered)
  • If the User click on Pay and pays within the Timer expires, the ticket is purchased and it appears in the /orders page
* StrikeCheckout component is used to Pay
  * It takes amount(assign order.ticket.price * 100), email(assign currentUser.email), stripekey and token(provides a callback which is used to do doRequest to /api/payments with order id as body and token id is passed as props) as props
* In /api/payments,
  * token and orderId is obtained from req.body
  * order is fetched from db using orderId
  * If order is not found, NotFoundError is thrown
  * If order.userId is not equal to req.currentUser.id, then NotAuthorizedError is thrown
  * If order.status is Cancelled, BadRequestError is thrown
  * Else, charge is created using stripe
  * Payment model is built using orderId and stripeId
  * It is then saved to db
  * **PaymentCreatedPublisher** publishes PaymentCreatedEvent with properties id(payment), orderId(payment.orderId) and stripeId(payment.stripeId) and then sends the id of the payment as response
    * **orders PaymentCreatedListener's** onMessage pulls data.orderId and fetches the order
    * If order is not found, an Error is thrown
    * Else, order sets its status as Complete and saves it to db
    * Ack message is then sent
  • The ticket also disappears from the index page
* In index.js LandingPage's getInitialProps, request is made to /api/tickets
* In index.ts of tickets, all the tickets with orderId undefined are fetched and is sent back

Flow for Ticket Updation

  • For ticket updation, PUT request to /api/ticket/:id
  • ticket is fetched from db using req.params.id
  • If ticket is not found, NotFoundError thrown
  • If ticket's orderId property is set, BadRequestError is thrown
  • If ticket's userId != req.currentUser.id, then NotAuthorizedError is thrown
  • Else ticket's title and price are set from req.body and is saved to db
  • TicketUpdatedPublisher publishes TicketUpdatedEvent with properties id(ticket), title, price, userId and version
  • The listener for this event is Orders service(using TicketUpdatedListener which starts listening in index.ts)
    • The orders TicketUpdatedListener's onMessage provides data to ticket model's(ticket model in orders service) findByEvent
      • The findByEvent method pulls out id and version and assigns to event object
      • Then using this id(event.id) and version(event.version - 1), the ticket is fetched from the db and returned
    • If ticket is not found, an Error is thrown
    • Else the title and price is pulled out of the data and is set on the ticket
    • The ticket is saved in the db and ack message is sent