diff --git a/packages/wallet/backend/src/app.ts b/packages/wallet/backend/src/app.ts index 5ccbb7fe6..86d51647a 100644 --- a/packages/wallet/backend/src/app.ts +++ b/packages/wallet/backend/src/app.ts @@ -313,20 +313,21 @@ export class App { cardController.getCardsByCustomer ) router.get('/cards/:cardId/details', isAuth, cardController.getCardDetails) - router.put('/cards/:cardId/lock', isAuth, cardController.lock) - router.put('/cards/:cardId/unlock', isAuth, cardController.unlock) + router.get( + '/cards/:cardId/transactions', + isAuth, + cardController.getCardTransactions + ) router.get('/cards/:cardId/limits', isAuth, cardController.getCardLimits) router.post( '/cards/:cardId/limits', isAuth, cardController.createOrOverrideCardLimits - ) router.get( - '/cards/:cardId/transactions', - isAuth, - cardController.getCardTransactions ) router.get('/cards/:cardId/pin', isAuth, cardController.getPin) router.post('/cards/:cardId/change-pin', isAuth, cardController.changePin) + router.put('/cards/:cardId/lock', isAuth, cardController.lock) + router.put('/cards/:cardId/unlock', isAuth, cardController.unlock) router.put( '/cards/:cardId/block', isAuth, diff --git a/packages/wallet/backend/src/card/controller.ts b/packages/wallet/backend/src/card/controller.ts index fb40ad7c7..bf6381a9c 100644 --- a/packages/wallet/backend/src/card/controller.ts +++ b/packages/wallet/backend/src/card/controller.ts @@ -103,76 +103,76 @@ export class CardController implements ICardController { } } - public getPin = async (req: Request, res: Response, next: NextFunction) => { + public getCardLimits = async ( + req: Request, + res: Response, + next: NextFunction + ) => { try { const userId = req.session.user.id - const { params, query } = await validate(getCardDetailsSchema, req) + const { params } = await validate(getCardLimitsSchema, req) const { cardId } = params - const { publicKeyBase64 } = query - const requestBody: ICardDetailsRequest = { cardId, publicKeyBase64 } - const cardPin = await this.cardService.getPin(userId, requestBody) - res.status(200).json(toSuccessResponse(cardPin)) + const limits = await this.cardService.getCardLimits(userId, cardId) + res.status(200).json(toSuccessResponse(limits)) } catch (error) { next(error) } } - public changePin = async ( + public createOrOverrideCardLimits = async ( req: Request, res: Response, next: NextFunction ) => { try { const userId = req.session.user.id - const { params, body } = await validate(changePinSchema, req) + const { params, body } = await validate( + createOrOverrideCardLimitsSchema, + req + ) const { cardId } = params - const { cypher } = body + const requestBody: ICardLimitRequest[] = body + + const result = await this.cardService.createOrOverrideCardLimits( + userId, + cardId, + requestBody + ) - const result = await this.cardService.changePin(userId, cardId, cypher) res.status(201).json(toSuccessResponse(result)) } catch (error) { next(error) } } - public getCardLimits = async ( - req: Request, - res: Response, - next: NextFunction - ) => { + public getPin = async (req: Request, res: Response, next: NextFunction) => { try { const userId = req.session.user.id - const { params } = await validate(getCardLimitsSchema, req) + const { params, query } = await validate(getCardDetailsSchema, req) const { cardId } = params + const { publicKeyBase64 } = query - const limits = await this.cardService.getCardLimits(userId, cardId) - res.status(200).json(toSuccessResponse(limits)) + const requestBody: ICardDetailsRequest = { cardId, publicKeyBase64 } + const cardPin = await this.cardService.getPin(userId, requestBody) + res.status(200).json(toSuccessResponse(cardPin)) } catch (error) { next(error) } } - public createOrOverrideCardLimits = async ( + public changePin = async ( req: Request, res: Response, next: NextFunction ) => { try { const userId = req.session.user.id - const { params, body } = await validate( - createOrOverrideCardLimitsSchema, - req - ) + const { params, body } = await validate(changePinSchema, req) const { cardId } = params - const requestBody: ICardLimitRequest[] = body - - const result = await this.cardService.createOrOverrideCardLimits( - userId, - cardId, - requestBody - ) + const { cypher } = body + const result = await this.cardService.changePin(userId, cardId, cypher) res.status(201).json(toSuccessResponse(result)) } catch (error) { next(error) diff --git a/packages/wallet/backend/src/card/service.ts b/packages/wallet/backend/src/card/service.ts index 66f1264db..e4ebb2fc8 100644 --- a/packages/wallet/backend/src/card/service.ts +++ b/packages/wallet/backend/src/card/service.ts @@ -34,6 +34,17 @@ export class CardService { return this.gateHubClient.getCardDetails(requestBody) } + async getCardTransactions( + userId: string, + cardId: string, + pageSize?: number, + pageNumber?: number + ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.getCardTransactions(cardId, pageSize, pageNumber) + } + async getCardLimits( userId: string, cardId: string @@ -53,17 +64,6 @@ export class CardService { return this.gateHubClient.createOrOverrideCardLimits(cardId, requestBody) } - async getCardTransactions( - userId: string, - cardId: string, - pageSize?: number, - pageNumber?: number - ): Promise { - await this.ensureWalletAddressExists(userId, cardId) - - return this.gateHubClient.getCardTransactions(cardId, pageSize, pageNumber) - } - async getPin( userId: string, requestBody: ICardDetailsRequest @@ -127,17 +127,4 @@ export class CardService { throw new NotFound('Card not found or not associated with the user.') } } - - private async ensureWalletAddressExists( - userId: string, - cardId: string - ): Promise { - const walletAddress = await this.walletAddressService.getByCardId( - userId, - cardId - ) - if (!walletAddress) { - throw new NotFound('Card not found or not associated with the user.') - } - } } diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index 71a1a88bf..a40e79a9f 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -407,6 +407,30 @@ export class GateHubClient { return this.request('GET', url) } + async getCardLimits(cardId: string): Promise { + const url = `${this.apiUrl}/v1/cards/${cardId}/limits` + + return this.request('GET', url, undefined, { + cardAppId: this.env.GATEHUB_CARD_APP_ID + }) + } + + async createOrOverrideCardLimits( + cardId: string, + requestBody: ICardLimitRequest[] + ): Promise { + const url = `${this.apiUrl}/v1/cards/${cardId}/limits` + + return this.request( + 'POST', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + } + async getPin( requestBody: ICardDetailsRequest ): Promise { @@ -471,30 +495,6 @@ export class GateHubClient { ) } - async getCardLimits(cardId: string): Promise { - const url = `${this.apiUrl}/v1/cards/${cardId}/limits` - - return this.request('GET', url, undefined, { - cardAppId: this.env.GATEHUB_CARD_APP_ID - }) - } - - async createOrOverrideCardLimits( - cardId: string, - requestBody: ICardLimitRequest[] - ): Promise { - const url = `${this.apiUrl}/v1/cards/${cardId}/limits` - - return this.request( - 'POST', - url, - JSON.stringify(requestBody), - { - cardAppId: this.env.GATEHUB_CARD_APP_ID - } - ) - } - async lockCard( cardId: string, reasonCode: LockReasonCode, diff --git a/packages/wallet/backend/tests/cards/controller.test.ts b/packages/wallet/backend/tests/cards/controller.test.ts index 9a7d7be36..3546b8872 100644 --- a/packages/wallet/backend/tests/cards/controller.test.ts +++ b/packages/wallet/backend/tests/cards/controller.test.ts @@ -511,44 +511,38 @@ describe('CardController', () => { }) }) - describe('lock', () => { - it('should lock card successfully', async () => { + describe('getPin', () => { + it('should get pin successfully', async () => { const next = jest.fn() - req.params.cardId = 'test-card-id' - req.body = { note: 'Lost my card' } - req.query = { reasonCode: 'LostCard' } + req.query = { publicKeyBase64: 'test-public-key' } - const mockResult = { status: 'locked' } - mockCardService.lock.mockResolvedValue(mockResult) + const mockedCardDetails: ICardDetailsResponse = { + cipher: 'encrypted-card-pin' + } - await cardController.lock(req, res, next) + mockCardService.getPin.mockResolvedValue(mockedCardDetails) - expect(mockCardService.lock).toHaveBeenCalledWith( - userId, - 'test-card-id', - 'LostCard', - { - note: 'Lost my card' - } - ) + await cardController.getPin(req, res, next) + expect(mockCardService.getPin).toHaveBeenCalledWith(userId, { + cardId: 'test-card-id', + publicKeyBase64: 'test-public-key' + }) expect(res.statusCode).toBe(200) expect(res._getJSONData()).toEqual({ success: true, message: 'SUCCESS', - result: mockResult + result: mockedCardDetails }) }) - it('should return 400 if reasonCode is missing', async () => { + it('should return 400 if cardId is missing', async () => { const next = jest.fn() - req.params.cardId = 'test-card-id' - req.body = { note: 'Lost my card' } - delete req.query.reasonCode + delete req.params.cardId - await cardController.lock(req, res, (err) => { + await cardController.getPin(req, res, (err) => { next(err) res.status(err.statusCode).json({ success: false, @@ -556,22 +550,20 @@ describe('CardController', () => { }) }) - expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') expect(res.statusCode).toBe(400) - expect(res._getJSONData()).toMatchObject({ - success: false, - message: 'Invalid input' - }) }) - it('should return 400 if reasonCode is invalid', async () => { + it('should return 400 if publicKeyBase64 is missing', async () => { const next = jest.fn() req.params.cardId = 'test-card-id' - req.query.reasonCode = 'InvalidCode' - req.body = { note: 'Lost my card' } + req.query = {} - await cardController.lock(req, res, (err) => { + await cardController.getPin(req, res, (err) => { next(err) res.status(err.statusCode).json({ success: false, @@ -579,22 +571,66 @@ describe('CardController', () => { }) }) - expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') expect(res.statusCode).toBe(400) - expect(res._getJSONData()).toMatchObject({ - success: false, - message: 'Invalid input' + }) + }) + + describe('changePin', () => { + it('should change pin successfully', async () => { + const next = jest.fn() + req.params.cardId = 'test-card-id' + req.body = { + cypher: 'test-cypher' + } + + mockCardService.changePin.mockResolvedValue({}) + + await cardController.changePin(req, res, next) + + expect(mockCardService.changePin).toHaveBeenCalledWith( + userId, + 'test-card-id', + 'test-cypher' + ) + expect(res.statusCode).toBe(201) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: {} }) }) - it('should return 400 if note is missing', async () => { + it('should return 400 if cardId is missing', async () => { + const next = jest.fn() + + delete req.params.cardId + + await cardController.changePin(req, res, (err) => { + next(err) + res.status(err.statusCode).json({ + success: false, + message: err.message + }) + }) + + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') + expect(res.statusCode).toBe(400) + }) + + it('should return 400 if cypher is missing', async () => { const next = jest.fn() req.params.cardId = 'test-card-id' - req.query.reasonCode = 'LostCard' req.body = {} - await cardController.lock(req, res, (err) => { + await cardController.changePin(req, res, (err) => { next(err) res.status(err.statusCode).json({ success: false, @@ -602,32 +638,33 @@ describe('CardController', () => { }) }) - expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) + expect(next).toHaveBeenCalled() + const error = next.mock.calls[0][0] + expect(error).toBeInstanceOf(BadRequest) + expect(error.message).toBe('Invalid input') expect(res.statusCode).toBe(400) - expect(res._getJSONData()).toMatchObject({ - success: false, - message: 'Invalid input' - }) }) }) - describe('unlock', () => { - it('should unlock the card successfully', async () => { + describe('lock', () => { + it('should lock card successfully', async () => { const next = jest.fn() req.params.cardId = 'test-card-id' - req.body = { note: 'Found my card' } + req.body = { note: 'Lost my card' } + req.query = { reasonCode: 'LostCard' } - const mockResult = { status: 'unlocked' } - mockCardService.unlock.mockResolvedValue(mockResult) + const mockResult = { status: 'locked' } + mockCardService.lock.mockResolvedValue(mockResult) - await cardController.unlock(req, res, next) + await cardController.lock(req, res, next) - expect(mockCardService.unlock).toHaveBeenCalledWith( + expect(mockCardService.lock).toHaveBeenCalledWith( userId, 'test-card-id', + 'LostCard', { - note: 'Found my card' + note: 'Lost my card' } ) @@ -639,13 +676,14 @@ describe('CardController', () => { }) }) - it('should return 400 if note is missing', async () => { + it('should return 400 if reasonCode is missing', async () => { const next = jest.fn() req.params.cardId = 'test-card-id' - req.body = {} + req.body = { note: 'Lost my card' } + delete req.query.reasonCode - await cardController.unlock(req, res, (err) => { + await cardController.lock(req, res, (err) => { next(err) res.status(err.statusCode).json({ success: false, @@ -660,40 +698,15 @@ describe('CardController', () => { message: 'Invalid input' }) }) - }) - - describe('getPin', () => { - it('should get pin successfully', async () => { - const next = jest.fn() - - req.query = { publicKeyBase64: 'test-public-key' } - - const mockedCardDetails: ICardDetailsResponse = { - cipher: 'encrypted-card-pin' - } - - mockCardService.getPin.mockResolvedValue(mockedCardDetails) - - await cardController.getPin(req, res, next) - - expect(mockCardService.getPin).toHaveBeenCalledWith(userId, { - cardId: 'test-card-id', - publicKeyBase64: 'test-public-key' - }) - expect(res.statusCode).toBe(200) - expect(res._getJSONData()).toEqual({ - success: true, - message: 'SUCCESS', - result: mockedCardDetails - }) - }) - it('should return 400 if cardId is missing', async () => { + it('should return 400 if reasonCode is invalid', async () => { const next = jest.fn() - delete req.params.cardId + req.params.cardId = 'test-card-id' + req.query.reasonCode = 'InvalidCode' + req.body = { note: 'Lost my card' } - await cardController.getPin(req, res, (err) => { + await cardController.lock(req, res, (err) => { next(err) res.status(err.statusCode).json({ success: false, @@ -701,20 +714,22 @@ describe('CardController', () => { }) }) - expect(next).toHaveBeenCalled() - const error = next.mock.calls[0][0] - expect(error).toBeInstanceOf(BadRequest) - expect(error.message).toBe('Invalid input') + expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toMatchObject({ + success: false, + message: 'Invalid input' + }) }) - it('should return 400 if publicKeyBase64 is missing', async () => { + it('should return 400 if note is missing', async () => { const next = jest.fn() req.params.cardId = 'test-card-id' - req.query = {} + req.query.reasonCode = 'LostCard' + req.body = {} - await cardController.getPin(req, res, (err) => { + await cardController.lock(req, res, (err) => { next(err) res.status(err.statusCode).json({ success: false, @@ -722,66 +737,50 @@ describe('CardController', () => { }) }) - expect(next).toHaveBeenCalled() - const error = next.mock.calls[0][0] - expect(error).toBeInstanceOf(BadRequest) - expect(error.message).toBe('Invalid input') + expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toMatchObject({ + success: false, + message: 'Invalid input' + }) }) }) - describe('changePin', () => { - it('should change pin successfully', async () => { + describe('unlock', () => { + it('should unlock the card successfully', async () => { const next = jest.fn() + req.params.cardId = 'test-card-id' - req.body = { - cypher: 'test-cypher' - } + req.body = { note: 'Found my card' } - mockCardService.changePin.mockResolvedValue({}) + const mockResult = { status: 'unlocked' } + mockCardService.unlock.mockResolvedValue(mockResult) - await cardController.changePin(req, res, next) + await cardController.unlock(req, res, next) - expect(mockCardService.changePin).toHaveBeenCalledWith( + expect(mockCardService.unlock).toHaveBeenCalledWith( userId, 'test-card-id', - 'test-cypher' + { + note: 'Found my card' + } ) - expect(res.statusCode).toBe(201) + + expect(res.statusCode).toBe(200) expect(res._getJSONData()).toEqual({ success: true, message: 'SUCCESS', - result: {} - }) - }) - - it('should return 400 if cardId is missing', async () => { - const next = jest.fn() - - delete req.params.cardId - - await cardController.changePin(req, res, (err) => { - next(err) - res.status(err.statusCode).json({ - success: false, - message: err.message - }) + result: mockResult }) - - expect(next).toHaveBeenCalled() - const error = next.mock.calls[0][0] - expect(error).toBeInstanceOf(BadRequest) - expect(error.message).toBe('Invalid input') - expect(res.statusCode).toBe(400) }) - it('should return 400 if cypher is missing', async () => { + it('should return 400 if note is missing', async () => { const next = jest.fn() req.params.cardId = 'test-card-id' req.body = {} - await cardController.changePin(req, res, (err) => { + await cardController.unlock(req, res, (err) => { next(err) res.status(err.statusCode).json({ success: false, @@ -789,11 +788,12 @@ describe('CardController', () => { }) }) - expect(next).toHaveBeenCalled() - const error = next.mock.calls[0][0] - expect(error).toBeInstanceOf(BadRequest) - expect(error.message).toBe('Invalid input') + expect(next).toHaveBeenCalledWith(expect.any(BadRequest)) expect(res.statusCode).toBe(400) + expect(res._getJSONData()).toMatchObject({ + success: false, + message: 'Invalid input' + }) }) }) diff --git a/packages/wallet/shared/src/types/card.ts b/packages/wallet/shared/src/types/card.ts index 2b43aaeff..11ef2baf0 100644 --- a/packages/wallet/shared/src/types/card.ts +++ b/packages/wallet/shared/src/types/card.ts @@ -18,6 +18,16 @@ export type BlockReasonCode = | 'IssuerRequestCustomerDeceased' | 'ProductDoesNotRenew' +export type CardLimitType = + | 'perTransaction' + | 'dailyOverall' + | 'weeklyOverall' + | 'monthlyOverall' + | 'dailyAtm' + | 'dailyEcomm' + | 'monthlyOpenScheme' + | 'nonEUPayments' + // Response for fetching card transactions export interface ITransaction { id: number @@ -52,13 +62,3 @@ export interface IGetTransactionsResponse { data: ITransaction[] pagination: IPagination } - -export type CardLimitType = - | 'perTransaction' - | 'dailyOverall' - | 'weeklyOverall' - | 'monthlyOverall' - | 'dailyAtm' - | 'dailyEcomm' - | 'monthlyOpenScheme' - | 'nonEUPayments'