Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#187419143-Feature Stripe Payment: Users should be able to buy and Pay with stripe #28

Merged
merged 1 commit into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/__test__/Payment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { configureStore } from "@reduxjs/toolkit";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";

import paymentReducer, {
makePayment,
handleSuccess,
} from "../redux/reducers/payment";

/* eslint-disable @typescript-eslint/default-param-last */
jest.mock("../redux/reducers/payment", () => ({
__esModule: true,
makePayment: jest
.fn()
.mockImplementation(() => ({ type: "mockMakePayment" })),
handleSuccess: jest
.fn()
.mockImplementation(() => ({ type: "mockHandleSuccess" })),
default: jest.fn().mockImplementation((state = {}, action) => {
switch (action.type) {
case "mockMakePayment":
case "mockHandleSuccess":
return {
...state,
loading: false,
data: { status: "success" },
error: null,
};
default:
return state;
}
}),
}));

describe("payment slice", () => {
let store;
let mockAxios;

beforeEach(() => {
store = configureStore({
reducer: {
payment: paymentReducer,
},
});

mockAxios = new MockAdapter(axios);
});

it("should handle makePayment", async () => {
const paymentData = { amount: 100 };
const mockResponse = { status: "success" };

mockAxios.onPost("/payment/checkout", paymentData).reply(200, mockResponse);

// const makePayment = require("../redux/reducers/payment").makePayment;
await store.dispatch(makePayment(paymentData));

const state = store.getState();
expect(state.payment.loading).toBe(false);
expect(state.payment.data).toEqual(mockResponse);
});
it("should handle handleSuccess", async () => {
const mockResponse = { sessionId: "testSessionId", userId: "testUserId" };

// const handleSuccess = require("../redux/reducers/payment").handleSuccess;
await store.dispatch(handleSuccess(mockResponse));

const state = store.getState();
expect(state.payment.loading).toBe(false);
expect(state.payment.data).toEqual({ status: "success" });
expect(state.payment.error).toBe(null);
});
});
100 changes: 100 additions & 0 deletions src/__test__/paymentApiSlice.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { configureStore } from "@reduxjs/toolkit";
import axios from "axios";

import paymentSlice, {
makePayment,
handleSuccess,
} from "../redux/reducers/payment";

jest.mock("../redux/api/api", () => ({
api: {
interceptors: {
response: {
use: jest.fn(),
},
request: {
use: jest.fn(),
},
},
},
}));

jest.mock("axios");

describe("paymentSlice", () => {
let store;

beforeEach(() => {
store = configureStore({
reducer: {
payment: paymentSlice,
},
});
});

it("handles successful makePayment", async () => {
const mockResponse = { data: { status: "success" } };
// @ts-ignore
axios.post.mockResolvedValueOnce(mockResponse);

await store.dispatch(
makePayment({
amount: 100,
}),
);

const state = store.getState();
expect(state.payment.loading).toBe(false);
});

it("handles failed makePayment", async () => {
console.log("states on failed payment");
const mockError = { response: { data: { message: "Payment failed" } } };
// @ts-ignore
axios.post.mockRejectedValueOnce(mockError);

await store.dispatch(
makePayment({
amount: 100,
}),
);

const state = store.getState();

expect(state.payment.loading).toBe(false);
});

it("handles successful handleSuccess", async () => {
const mockResponse = { data: { status: "success" } };
// @ts-ignore
axios.get.mockResolvedValueOnce(mockResponse);

await store.dispatch(
handleSuccess({
sessionId: "testSessionId",
userId: "testUserId",
}),
);

const state = store.getState();
expect(state.payment.loading).toBe(false);
});

it("handles failed handleSuccess", async () => {
const mockError = {
response: { data: { message: "handleSuccess failed" } },
};
// @ts-ignore
axios.get.mockRejectedValueOnce(mockError);

await store.dispatch(
handleSuccess({
sessionId: "testSessionId",
userId: "testUserId",
}),
);

const state = store.getState();
expect(state.payment.loading).toBe(false);
});
});
4 changes: 2 additions & 2 deletions src/pages/CartManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IoChevronUpOutline, IoChevronDownSharp } from "react-icons/io5";
import { MdOutlineClose } from "react-icons/md";
import { ToastContainer, toast } from "react-toastify";
import { AxiosError } from "axios";
import { Link } from "react-router-dom";

import { RootState } from "../redux/store";
import {
Expand Down Expand Up @@ -50,7 +51,6 @@ const CartManagement: React.FC<IProductCardProps> = () => {
</div>
);
}
console.log(userCart);

const handleDelete = async () => {
await dispatch(cartDelete());
Expand Down Expand Up @@ -239,7 +239,7 @@ const CartManagement: React.FC<IProductCardProps> = () => {
</div>
<hr className="w-full border-t border-gray-300 mt-2" />
<div className="bg-[#DB4444] text-white rounded-sm px-2 md:px-2 py-2 hover:border-[0.5px] mt-8 cursor-pointer mx-auto md:text-[14px]">
Proceed to Checkout
<Link to="/payment">Proceed to Checkout</Link>
</div>
</div>
)}
Expand Down
178 changes: 178 additions & 0 deletions src/pages/paymentPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import { ToastContainer, toast } from "react-toastify";

import { RootState } from "../redux/store";
import HeaderInfo from "../components/common/header/Info";
import Footer from "../components/common/footer/Footer";
import { useAppDispatch } from "../redux/hooks";
import { makePayment, handleSuccess } from "../redux/reducers/payment";
import { cartManage } from "../redux/reducers/cartSlice";
import Spinner from "../components/common/auth/Loader";

const Payment = () => {
const loading = useSelector((state: RootState) => state.payment.loading);
const userCart = useSelector((state: RootState) => state!.cart.data);
const dispatch = useAppDispatch();
const totalPrice = userCart.reduce((total, item) => total + item.price, 0);

const handlePayment = () => {
try {
dispatch(makePayment({ totalPrice, userCart })).then((response) => {
if (response.payload.sessionUrl) {
toast(`${response.payload.message}\n Redirecting to stripe payment`);

setTimeout(() => {
window.location.href = response.payload.sessionUrl;
}, 3000);
} else {
toast(response.payload.message);
}
});
} catch (err) {
toast.error("Failed to make payment");
}
};

useEffect(() => {
dispatch(cartManage());
}, [dispatch]);

const total = userCart.reduce(
// @ts-ignore
(acc, item) => acc + item.product?.price * item.quantity,
0,
);

return (
<div className="px-[2%] md:px-[4%] parent-container h-screen overflow-auto">
<ToastContainer />
<div className="pt-8">
<h2>
<Link to="/">Home</Link>
{' '}
/
<Link to="/carts">Carts</Link>
{' '}
/ payment
</h2>
</div>
<div className="w-full sm:w-[50%] md:w-[38%] flex flex-col justify-center items-start mt-9 rounded-sm p-4 hover:border-[1.5px]">
<h2 className="text-lg font-semibold mb-4 text-left">
Checkout Details
</h2>
{userCart.length === 0 ? (
<tr>
<td colSpan={4} className="text-center">
No items in the cart 😎
</td>
</tr>
) : (
userCart.map((item: any) => (
<tr
key={item.id}
className="flex gap-10 justify-between w-full mb-2"
>
<td className="text-left py-3">
<div className="flex items-center">
<img
data-testId="img-cart"
className="w-12"
src={item.product?.images[0]}
alt={item.product?.name}
/>
<span className="mx-2 hidden md:block text-[9px] md:text-normal">
{item.product?.name.length > 8
? `${item.product?.name.slice(0, 8)}...`
: item.product?.name}
</span>
</div>
</td>
<td className="text-left text-[14px]">
<h2 data-testId="price-cart">
RWF
{item.product?.price}
</h2>
</td>
</tr>
))
)}
<div className="flex gap-10 justify-between w-full mb-2">
<h1 data-testId="subtotal" className="text-left">
Subtotal
</h1>
<span>
RWF
{total}
</span>
</div>
<hr className="w-full border-t border-gray-300 mb-2" />
<div className="flex gap-10 justify-between w-full mb-2 ext-[9px] md:text-normal">
<h1 data-testId="shipping" className="text-left">
Shipping
</h1>
<span>Free</span>
</div>
<hr className="w-full border-t border-gray-300 mb-2" />
<div className="flex gap-10 justify-between w-full">
<h1 data-testId="total" className="text-left">
Total
</h1>
<span className="">
RWF
{total}
</span>
</div>
<hr className="w-full border-t border-gray-300 mt-2" />
<div className="bg-[#DB4444] text-white rounded-sm px-2 md:px-2 py-2 hover:border-[0.5px] mt-8 cursor-pointer mx-auto md:text-[14px]">
<button onClick={handlePayment}>
{loading ? "Processing..." : "Pay with Stripe"}
</button>
</div>
</div>
<div className="bg-gray-200 w-100% sm:w-[100%] h-[1px] mt-[0.1%]" />
</div>
);
};

const SuccessfulPayment = () => {
const dispatch = useAppDispatch();
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get("sessionId");
const userId = urlParams.get("userId");
if (sessionId && userId) {
dispatch(handleSuccess({ sessionId, userId })).then((action: any) => {
if (handleSuccess.fulfilled.match(action)) {
console.log("Payment Data", action.payload);
} else if (handleSuccess.rejected.match(action)) {
console.error("Failed to fetch payment data", action.error);
}
});
}
}, [dispatch]);

return (
<section className="flex items-center justify-center py-32 bg-gray-100 md:m-0 px-4 ">
<div className="bg-white p-6 rounded shadow-md text-center">
<h1 className="text-2xl font-medium text-red-500">
Payment Was Successful !!!
</h1>
<p className="mt-4">
Checkout Details about your Order More details was sent to your Email!
</p>
<p className="mt-2">Thank you for shopping with us.</p>

<Link to="/orders">
<button className="mt-4 inline-block px-4 py-2 text-white bg-red-500 rounded transition-colors duration-300 cursor-pointer hover:bg-green-600">
Checkout your Order
</button>
</Link>
</div>
</section>
);
};

export default Payment;
export { SuccessfulPayment };
4 changes: 0 additions & 4 deletions src/redux/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { set } from "react-hook-form";
import { useState, useEffect } from "react";
import { toast } from "react-toastify";

import store from "../store";

import api from "./action";

let navigateFunction = null;
Expand Down
Loading
Loading