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