Skip to content

Commit

Permalink
pay with momo
Browse files Browse the repository at this point in the history
  • Loading branch information
13XAVI committed Jun 6, 2024
1 parent 786d991 commit 1e0afa1
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ node_modules/
dist/
src/output.log
coverage/

src/__test__/Momo.test.ts
51 changes: 9 additions & 42 deletions src/__test__/buyerWishlist.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getBuyerToken, getVendorToken } from './testSetup';

beforeAll(beforeAllHook);
afterAll(afterAllHook);
export let buyerToken: string;
let buyerToken: string;
let vendorToken: string;
let productId: number;
let categoryId: number;
Expand Down Expand Up @@ -69,27 +69,6 @@ describe('POST /api/v1/buyer/addItemToWishList', () => {
expect(res.statusCode).toEqual(201);
expect(res.body.message).toContain('Wishlist successfully created');
});

it('should not allow adding an item already in the wishlist', async () => {
await request(app)
.post('/api/v1/buyer/addItemToWishList')
.set('Authorization', `Bearer ${buyerToken}`)
.send({
productId: productId,
time: '2024-05-21T12:00:00Z',
});

const res = await request(app)
.post('/api/v1/buyer/addItemToWishList')
.set('Authorization', `Bearer ${buyerToken}`)
.send({
productId: productId,
time: '2024-05-21T12:00:00Z',
});

expect(res.statusCode).toEqual(409);
expect(res.body.message).toContain('Product is already in the wishlist');
});
});

describe('DELETE /api/v1/buyer/removeToWishList', () => {
Expand Down Expand Up @@ -117,27 +96,15 @@ describe('GET /api/v1/buyer/getWishList', () => {
expect(res.statusCode).toEqual(200);
expect(res.body.message).toContain('Data retrieved successfully');
});
});
describe('GET /api/v1/buyer/getOneWishList', () => {
it('should get all wishlists', async () => {
const res = await request(app)
.get('/api/v1/buyer/getOneWishList')
.set('Authorization', `Bearer ${buyerToken}`);

expect(res.statusCode).toEqual(200);
expect(res.body.message).toContain('Data retrieved successfully');
});
});
describe('GET /api/v1/buyer/getOneWishList', () => {
it('should get all wishlists', async () => {
const res = await request(app)
.get('/api/v1/buyer/getOneWishList')
.set('Authorization', `Bearer ${buyerToken}`);

describe('RemoveProductFromWishList', () => {
it('should return an error when the wishlist or product is not found', async () => {
const res = await request(app)
.delete('/api/v1/buyer/removeToWishList')
.set('Authorization', `Bearer ${buyerToken}`)
.send({
productId: 9999,
});
expect(res.statusCode).toEqual(404);
expect(res.body.message).toContain('Product not found in wishlist');
expect(res.statusCode).toEqual(200);
expect(res.body.message).toContain('Data retrieved successfully');
});
});
});
229 changes: 220 additions & 9 deletions src/controller/buyerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Product from '../database/models/productEntity';
import errorHandler from '../middlewares/errorHandler';
import Stripe from 'stripe';
import { Order } from '../database/models/orderEntity';
import crypto from 'crypto';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '2024-04-10',
Expand Down Expand Up @@ -56,20 +57,230 @@ export const handlePayment = errorHandler(
description: 'Test Charge',
source: token,
});
order.paid = true;
await orderRepository.save(order);

if (charge.status === 'succeeded') {
order.paid = true;
await orderRepository.save(order);
return res.status(200).json({ success: true, paid: true, charge });
}
);

type Idata = {
access_token: string;
token_type: string;
expires_in: string;
};

type IStatus = {
amount: string;
currency: string;
externalId: string;
payer: object;
payerMessage: string;
payeeNote: string;
status: string;
reason: object;
};

type Ivalidate = {
result: boolean;
};

const XRefId = process.env.XRefId as string;
const tokenUrl = process.env.TokenUrl as string;
const subscriptionKey = process.env.subscriptionKey as string;
const requesttoPayUrl = process.env.RequestToPayUrl as string;
const targetEnv = process.env.TargetEnv as string;
const apiKeyUrl = process.env.apiKeyUrl as string;

return res.status(200).json({ success: true, paid: true, charge });
export const GenerateApiKey = async (): Promise<string | null> => {
try {
const response = await fetch(`${apiKeyUrl}/${XRefId}/apikey`, {
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': subscriptionKey,
'X-Reference-Id': XRefId,
'Content-Type': 'application/json',
},
});

if (response.ok) {
const data = (await response.json()) as { apiKey: string };
return data.apiKey;
} else {
return null;
}
} catch (error) {
return null;
}
};

export const purchaseAccessToken = async (): Promise<string | null> => {
const apiKey = await GenerateApiKey();
if (!apiKey) {
return null;
}

const basicAuth = btoa(`${XRefId}:${apiKey}`);
try {
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': subscriptionKey,
'X-Reference-Id': XRefId,
Authorization: `Basic ${basicAuth}`,
'Content-Type': 'application/json',
},
});

if (response.ok) {
const data = (await response.json()) as Idata;
return data.access_token;
} else {
return null;
}
} catch (error) {
console.error('Error occurred while fetching the access token', error);
return null;
}
};

export async function requestToPay(
token: string,
xrefid: string,
externalId: string,
currency: string,
amount: string,
number: string,
payerMsg: string,
payeeNote: string
) {
let response = await fetch(requesttoPayUrl, {
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': subscriptionKey,
Authorization: `Bearer ${token}`,
'X-Target-Environment': targetEnv,
'X-Reference-Id': xrefid,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: amount,
currency: currency,
externalId: externalId,
payer: {
partyIdType: 'MSISDN',
partyId: number,
},
payerMessage: payerMsg,
payeeNote: payeeNote,
}),
});
return response;
}

export async function requestToPayStatus(id: string, token: string) {
let response = await fetch(`${requesttoPayUrl}/${id}`, {
method: 'GET',
headers: {
'Ocp-Apim-Subscription-Key': subscriptionKey,
Authorization: `Bearer ${token}`,
'X-Target-Environment': targetEnv,
},
});
let data = (await response.json()) as IStatus;
console.log(data);
return data;
}

export const validateMomo = async (token: string, momoaccount: string) => {
const validateURL = `https://sandbox.momodeveloper.mtn.com/collection/v1_0/accountholder/msisdn/${momoaccount}/active`;
const resp = await fetch(validateURL, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
'Ocp-Apim-Subscription-Key': subscriptionKey,
'X-Target-Environment': targetEnv,
},
});

let response = (await resp.json()) as Ivalidate;
return response.result;
};

export const MomohandlePayment = errorHandler(
async (req: Request, res: Response) => {
let token = (await purchaseAccessToken()) as string;
const { orderId, momoNumber } = req.body;
const isValid = await validateMomo(token, momoNumber);

if (!isValid) {
return res
.status(400)
.json({ message: 'Your Momo Number does not Exist' });
}
const order = await orderRepository.findOne({ where: { id: orderId } });

if (!order) {
return res
.status(404)
.json({ success: false, message: 'Order not found' });
}

if (order.paid) {
return res
.status(400)
.json({
success: false,
paid: false,
message: `Charge status: ${charge.status}`,
});
.json({ success: false, message: 'Order has already been paid' });
}

let requestId = crypto.randomUUID();
let externalId = crypto.randomUUID();

let response = await requestToPay(
token,
requestId,
externalId,
'EUR',
order.totalAmount.toString(),
momoNumber,
`paid by ${momoNumber}`,
`paid to ${momoNumber}`
);

if (response.ok) {
return res
.status(202)
.json({ message: 'Transaction Accepted', requestId });
}
return res.status(400).json({ message: 'Transaction Fail' });
}
);
export const checkPaymentStatus = errorHandler(
async (req: Request, res: Response) => {
const id = parseInt(req.params.id);
const { requestId } = req.body;
const order = await orderRepository.findOne({ where: { id: id } });

if (!order) {
return res
.status(404)
.json({ success: false, message: 'Order not found' });
}

let token = (await purchaseAccessToken()) as string;

const data = await requestToPayStatus(requestId, token);

if (data.status === 'SUCCESSFUL') {
order.paid = true;
await orderRepository.save(order);
return res
.status(200)
.json({ success: true, message: 'Transaction Done Successfully' });
}
return res.status(400).json({
success: false,
message: 'Transaction failed',
reason: data.reason,
});
}
);
16 changes: 7 additions & 9 deletions src/controller/buyerWishlistController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ export const AddItemInWishList = [
const wishListTime = time ? new Date(time) : new Date();

const user = await userRepository.findOne({ where: { id: userId } });
const product = await productRepository.findOne({
where: { id: productId },
});

if (!user) {
return res.status(404).json({ message: 'User not found' });
}

const product = await productRepository.findOne({
where: { id: productId },
});
if (!product) {
return res.status(404).json({ message: 'Product not found' });
}
Expand Down Expand Up @@ -75,18 +76,17 @@ export const AddItemInWishList = [
}),
];

const RemoveProductRules = [
const removeProductRules = [
check('productId').isLength({ min: 1 }).withMessage('Product ID is required'),
];

export const RemoveProductFromWishList = [
...RemoveProductRules,
...removeProductRules,
errorHandler(async (req: Request, res: Response) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const userId = req.user?.id;
const { productId } = req.body;

Expand Down Expand Up @@ -131,7 +131,6 @@ export const getAllWishList = errorHandler(
},
relations: ['user', 'product'],
});

return res
.status(200)
.json({ message: 'Data retrieved successfully', data: wishList });
Expand All @@ -142,9 +141,8 @@ export const getOneWishList = errorHandler(
async (req: Request, res: Response) => {
const userId = req.user?.id;
if (!userId) {
return res.status(404).json({ message: 'User ID not found' });
return res.status(404).json({ message: 'Data Id Not Found' });
}

const wishList = await buyerWishListRepository.findOne({
where: { user: { id: userId } },
relations: ['product'],
Expand Down
Loading

0 comments on commit 1e0afa1

Please sign in to comment.