diff --git a/.github/workflows/CI - Tests - Auth Service.yml b/.github/workflows/CI - Tests - Auth Service.yml
new file mode 100644
index 0000000..c33ab5f
--- /dev/null
+++ b/.github/workflows/CI - Tests - Auth Service.yml
@@ -0,0 +1,13 @@
+# ========================================= Main Branch ::: CI - Tests - Auth Service =========================================
+
+name: CI - Tests - Auth Service
+
+on: pull_request
+
+jobs:
+ Production-Branch-Pre-Integration-Tests:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - run: cd auth && npm install && npm run test:ci
diff --git a/auth/package.json b/auth/package.json
index 604068e..b1cfa63 100644
--- a/auth/package.json
+++ b/auth/package.json
@@ -5,7 +5,8 @@
"main": "index.js",
"scripts": {
"start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts",
- "test": "jest --watchAll --no-cache"
+ "test": "jest --watchAll --no-cache",
+ "test:ci": "jest"
},
"jest": {
"preset": "ts-jest",
diff --git a/client/components/header.js b/client/components/header.js
index b9dac96..77471a3 100644
--- a/client/components/header.js
+++ b/client/components/header.js
@@ -2,9 +2,11 @@ import Link from "next/link";
export default ({ currentUser }) => {
const Links = [
- !currentUser && { label: "Sign Up", href: "auth/signup" },
- !currentUser && { label: "Sign In", href: "auth/signin" },
- currentUser && { label: "Sign Out", href: "auth/signout" },
+ !currentUser && { label: "Sign Up", href: "/auth/signup" },
+ !currentUser && { label: "Sign In", href: "/auth/signin" },
+ currentUser && { label: "Sell Tickets", href: "/tickets/new" },
+ currentUser && { label: "My Orders", href: "/orders" },
+ currentUser && { label: "Sign Out", href: "/auth/signout" },
]
.filter((validLinks) => validLinks)
.map(({ label, href }) => {
diff --git a/client/hooks/use-request.js b/client/hooks/use-request.js
index 8d528d4..ed28ccb 100644
--- a/client/hooks/use-request.js
+++ b/client/hooks/use-request.js
@@ -4,11 +4,11 @@ import axios from "axios";
export default ({ url, method, body, onSuccess }) => {
const [errors, setErrors] = useState(null);
- const makeRequest = async () => {
+ const makeRequest = async (props={}) => {
try {
setErrors(null); // Setting error to null initially to prevent errors from being displayed always.
- const response = await axios[method](url, body);
+ const response = await axios[method](url, {...body, ...props});
// If the call back exist, then return the call back with response data.
if (onSuccess) {
diff --git a/client/package-lock.json b/client/package-lock.json
index 950faf0..d8288fc 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -12,8 +12,10 @@
"axios": "^1.5.1",
"bootstrap": "^5.3.2",
"next": "^13.5.4",
+ "prop-types": "^15.8.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-stripe-checkout": "^2.6.3"
}
},
"node_modules/@next/env": {
@@ -400,6 +402,14 @@
}
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -432,6 +442,16 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -460,6 +480,16 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/react-stripe-checkout": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/react-stripe-checkout/-/react-stripe-checkout-2.6.3.tgz",
+ "integrity": "sha512-lnsCaAdlmwPGGMbQoI8FXtQUgEm+ktzPZ/ipAw4j0HYf80kef7CivGx6QitmgEn99/aa5hI/dmVXwfVZW/Mzfg=="
+ },
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
diff --git a/client/package.json b/client/package.json
index af83111..4ff3d98 100644
--- a/client/package.json
+++ b/client/package.json
@@ -13,7 +13,9 @@
"axios": "^1.5.1",
"bootstrap": "^5.3.2",
"next": "^13.5.4",
+ "prop-types": "^15.8.1",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react-stripe-checkout": "^2.6.3"
}
}
diff --git a/client/pages/_app.js b/client/pages/_app.js
index 187ad3d..c7c8c7c 100644
--- a/client/pages/_app.js
+++ b/client/pages/_app.js
@@ -7,7 +7,9 @@ const AppComponent = ({ Component, pageProps, currentUser }) => {
return (
);
};
@@ -20,7 +22,11 @@ AppComponent.getInitialProps = async (appContext) => {
// If the child component has getInitialProps method, then execute it manually from here.
let pageProps = {};
if (appContext.Component.getInitialProps) {
- pageProps = await appContext.Component.getInitialProps(appContext.ctx);
+ pageProps = await appContext.Component.getInitialProps(
+ appContext.ctx,
+ client,
+ response.data.currentUser
+ );
}
return { pageProps, currentUser: response.data.currentUser };
diff --git a/client/pages/index.js b/client/pages/index.js
index 6163abf..2401514 100644
--- a/client/pages/index.js
+++ b/client/pages/index.js
@@ -1,34 +1,56 @@
-import buildClient from "../api/build-client";
+import Link from 'next/link';
-const IndexPage = ({ currentUser }) => {
+const IndexPage = ({ currentUser, tickets }) => {
if (!currentUser) {
return (
<>
Landing Page
-
+
Signed Out
>
);
}
+ let ticketList;
+ if (currentUser) {
+ ticketList = tickets.map((ticket) => {
+ return (
+
+ {ticket.title} |
+ {ticket.price} |
+
+
+ View
+
+ |
+
+ );
+ });
+ }
+
return (
<>
- Landing Page
-
Welcome {currentUser.email}
+
+ Tickets
+
+
+
+ Title |
+ Price |
+ Action |
+
+
+
+ {currentUser && ticketList}
+
>
);
};
-IndexPage.getInitialProps = async (context) => {
- try {
- const client = buildClient(context);
- const response = await client.get("/api/users/currentuser");
- return response.data;
- } catch (err) {
- console.error("Error in making request to get current user:", err.message);
- return { currentUser: null };
- }
+IndexPage.getInitialProps = async (context, client, currentUser) => {
+ const { data } = await client.get("/api/tickets");
+ return { tickets: data };
};
export default IndexPage;
diff --git a/client/pages/orders/[orderId].js b/client/pages/orders/[orderId].js
new file mode 100644
index 0000000..de0588a
--- /dev/null
+++ b/client/pages/orders/[orderId].js
@@ -0,0 +1,66 @@
+import { useEffect, useState } from "react";
+import Router from "next/router";
+import StripeCheckout from "react-stripe-checkout";
+import useRequest from "../../hooks/use-request";
+
+const OrderShow = ({ order, currentUser }) => {
+ const [timeLeft, setTimeLeft] = useState(0);
+ const [makeRequest, errors] = useRequest({
+ url: "/api/payments",
+ method: "post",
+ body: {
+ orderId: order.id,
+ },
+ onSuccess: (payment) => {
+ Router.push("/orders");
+ },
+ });
+
+ useEffect(() => {
+ const findTimeLeft = () => {
+ const milliSecondsLeft = new Date(order.expiresAt) - new Date();
+ const secondsLeft = Math.round(milliSecondsLeft / 1000);
+
+ setTimeLeft(secondsLeft);
+ };
+
+ findTimeLeft();
+ const timer = setInterval(findTimeLeft, 1000);
+
+ return () => {
+ clearInterval(timer);
+ };
+ }, [order]);
+
+ if (timeLeft < 0) {
+ return (
+
+
Order Expired !!!
+
+ );
+ }
+
+ return (
+
+ Time left to complete payment: {timeLeft} seconds.
+ {
+ makeRequest({ token: token.id });
+ }}
+ stripeKey="pk_test_51O53zRSJtuYafghXhYNZzaJqYAh6afqRduQ3UAMs6Wm4vkv30ayq09gBPgU3jYkQPXrofQa9aRbIlb4uuCp3FC6O000J86xaKc"
+ amount={order.ticket.price * 100}
+ email={currentUser.email}
+ />
+ {errors}
+
+ );
+};
+
+OrderShow.getInitialProps = async (context, client) => {
+ const { orderId } = context.query;
+ const { data } = await client.get(`/api/orders/${orderId}`);
+
+ return { order: data };
+};
+
+export default OrderShow;
diff --git a/client/pages/orders/index.js b/client/pages/orders/index.js
new file mode 100644
index 0000000..22f886b
--- /dev/null
+++ b/client/pages/orders/index.js
@@ -0,0 +1,23 @@
+const OrderIndex = ({ orders }) => {
+ return (
+ <>
+
+ {orders.map((order) => {
+ return (
+ -
+ {order.ticket.title} - {order.status}
+
+ );
+ })}
+
+ >
+ );
+};
+
+OrderIndex.getInitialProps = async (context, client) => {
+ const response = await client.get("/api/orders");
+
+ return { orders: response.data };
+};
+
+export default OrderIndex;
diff --git a/client/pages/tickets/[ticketId].js b/client/pages/tickets/[ticketId].js
new file mode 100644
index 0000000..212f31f
--- /dev/null
+++ b/client/pages/tickets/[ticketId].js
@@ -0,0 +1,40 @@
+import Router from "next/router";
+import useRequest from "../../hooks/use-request";
+
+const TicketShow = ({ ticket }) => {
+ const [makeRequest, errors] = useRequest({
+ url: "/api/orders",
+ method: "post",
+ body: {
+ ticketId: ticket.id,
+ },
+ onSuccess: (orderData) => {
+ Router.push("/orders/[orderId]", `/orders/${orderData.id}`);
+ },
+ });
+
+ const handleClick = async () => {
+ makeRequest();
+ };
+
+ return (
+
+
{ticket.title}
+ Price: {ticket.price} INR
+ {errors}
+
+
+ );
+};
+
+TicketShow.getInitialProps = async (context, client) => {
+ const { ticketId } = context.query;
+
+ const { data } = await client.get(`/api/tickets/${ticketId}`);
+
+ return { ticket: data };
+};
+
+export default TicketShow;
diff --git a/client/pages/tickets/new.js b/client/pages/tickets/new.js
new file mode 100644
index 0000000..1a2a197
--- /dev/null
+++ b/client/pages/tickets/new.js
@@ -0,0 +1,70 @@
+import { useState } from "react";
+import Router from "next/router";
+import useRequest from "../../hooks/use-request";
+
+const NewTicket = () => {
+ const [title, setTitle] = useState("");
+ const [price, setPrice] = useState("");
+ const [makeRequest, errors] = useRequest({
+ url: "/api/tickets",
+ method: "post",
+ body: {
+ title: title,
+ price: price,
+ },
+ onSuccess: () => {
+ Router.push("/");
+ },
+ });
+
+ const handleSubmit = (event) => {
+ event.preventDefault();
+
+ if (!title || !price) {
+ return;
+ }
+
+ makeRequest();
+ };
+
+ const roundOff = () => {
+ const roundedPrice = parseFloat(price);
+
+ if (isNaN(roundedPrice)) {
+ // If price is not a number
+ return;
+ }
+
+ setPrice(roundedPrice.toFixed(2));
+ };
+ return (
+
+ );
+};
+
+export default NewTicket;
diff --git a/orders/package-lock.json b/orders/package-lock.json
index 7ef1b4f..0f6c764 100644
--- a/orders/package-lock.json
+++ b/orders/package-lock.json
@@ -1368,9 +1368,9 @@
"dev": true
},
"node_modules/@bookmyseat/common": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/@bookmyseat/common/-/common-1.0.11.tgz",
- "integrity": "sha512-1DI2vS8fTTnsvsl5N1IfMvGdVJs5WGEVNCTuyzISaf3B1UoE/qLHKrvCrallawXk4bTOLyr6MTo1DcWyS0xYfQ==",
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@bookmyseat/common/-/common-1.0.12.tgz",
+ "integrity": "sha512-HuKJrOIAQagz/ax8ZeV5og7ms8qhTUH5jXVW9B29e7h3izP91DUSmh9KBrxNQpTyI93RUgh6DhZ31InuIREViw==",
"dependencies": {
"@types/cookie-session": "^2.0.45",
"@types/express": "^4.17.18",
diff --git a/orders/package.json b/orders/package.json
index 010e589..0860052 100644
--- a/orders/package.json
+++ b/orders/package.json
@@ -5,7 +5,8 @@
"main": "index.js",
"scripts": {
"start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts",
- "test": "jest --watchAll --no-cache"
+ "test": "jest --watchAll --no-cache",
+ "test:ci": "jest"
},
"jest": {
"preset": "ts-jest",
diff --git a/orders/src/events/listeners/expiration-complete-listener.ts b/orders/src/events/listeners/expiration-complete-listener.ts
index d1e6ab8..09e48da 100644
--- a/orders/src/events/listeners/expiration-complete-listener.ts
+++ b/orders/src/events/listeners/expiration-complete-listener.ts
@@ -25,6 +25,11 @@ export class OrderExpirationListener extends Listener {
throw new Error("Order not found !!!");
}
+ // If the order is already paid and marked complete, return early and acknowledge the event to NATS.
+ if (order.status === OrderStatus.Complete) {
+ return msg.ack();
+ }
+
order.set({ status: OrderStatus.Cancelled });
await order.save();
diff --git a/orders/src/events/listeners/payment-created-listener.ts b/orders/src/events/listeners/payment-created-listener.ts
new file mode 100644
index 0000000..76ff3ae
--- /dev/null
+++ b/orders/src/events/listeners/payment-created-listener.ts
@@ -0,0 +1,39 @@
+import { Message } from "node-nats-streaming";
+import {
+ Listener,
+ EventSubjects,
+ PaymentCreatedEvent,
+ NotFoundError,
+ OrderStatus,
+} from "@bookmyseat/common";
+
+import { queueGroupName } from "../order-service-queue-group-name";
+import { Order } from "../../models/order";
+
+export class PaymentCreatedListener extends Listener {
+ readonly subject = EventSubjects.PaymentCreated;
+
+ queueGroupName = queueGroupName;
+
+ async onMessage(data: PaymentCreatedEvent["data"], msg: Message) {
+ try {
+ // Destructure the ticket id, title, and price from data argument.
+ const { id, version, orderId, stripeId } = data;
+
+ const order = await Order.findById(data.orderId);
+
+ if (!order) {
+ throw new NotFoundError();
+ }
+
+ order.set({ status: OrderStatus.Complete });
+
+ await order.save();
+
+ // Acknowledge the Payment Created event to NATS server.
+ msg.ack();
+ } catch (error) {
+ console.error("Error processing PaymentCreatedEvent", error);
+ }
+ }
+}
diff --git a/orders/src/index.ts b/orders/src/index.ts
index 3aaf385..dc9303d 100644
--- a/orders/src/index.ts
+++ b/orders/src/index.ts
@@ -7,6 +7,7 @@ import { natsClient } from "./nats-client";
import { TicketCreatedListener } from "./events/listeners/ticket-created-listener";
import { TicketUpdatedListener } from "./events/listeners/ticket-updated-listener";
import { OrderExpirationListener } from "./events/listeners/expiration-complete-listener";
+import { PaymentCreatedListener } from "./events/listeners/payment-created-listener";
const startServer = async () => {
// Server Configuration
@@ -84,6 +85,7 @@ const startServer = async () => {
new TicketCreatedListener(natsClient.client).listen();
new TicketUpdatedListener(natsClient.client).listen();
new OrderExpirationListener(natsClient.client).listen();
+ new PaymentCreatedListener(natsClient.client).listen();
} catch (err) {
console.error(
`Error Connecting ${SERVICE_NAME} Service to NATS CLUSTER: ${NATS_CLUSTER_ID}:`,
diff --git a/tickets/package-lock.json b/tickets/package-lock.json
index 2bf402a..400690f 100644
--- a/tickets/package-lock.json
+++ b/tickets/package-lock.json
@@ -1368,9 +1368,9 @@
"dev": true
},
"node_modules/@bookmyseat/common": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/@bookmyseat/common/-/common-1.0.10.tgz",
- "integrity": "sha512-+uQeZIU6BcJ5YDOCiWoVY2xB2Qc0oa3EZgHRSZt9guUHq0oSE7nKA3OX/iE+R9vee7ffOvQUWWjNfSTuRTwYlA==",
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@bookmyseat/common/-/common-1.0.12.tgz",
+ "integrity": "sha512-HuKJrOIAQagz/ax8ZeV5og7ms8qhTUH5jXVW9B29e7h3izP91DUSmh9KBrxNQpTyI93RUgh6DhZ31InuIREViw==",
"dependencies": {
"@types/cookie-session": "^2.0.45",
"@types/express": "^4.17.18",
diff --git a/tickets/src/routes/index.ts b/tickets/src/routes/index.ts
index 600d265..525b07b 100644
--- a/tickets/src/routes/index.ts
+++ b/tickets/src/routes/index.ts
@@ -4,7 +4,9 @@ import { Ticket } from "../models/ticket";
const router = express.Router();
router.get("/api/tickets", async (req: Request, res: Response) => {
- const tickets = await Ticket.find({});
+
+ // Fetch all Tickets that are available to make a booking (ie, which are not reserved by any order)
+ const tickets = await Ticket.find({orderId: undefined});
res.send(tickets);
});