diff --git a/packages/wallet/backend/src/app.ts b/packages/wallet/backend/src/app.ts index 978377df0..9d5c82786 100644 --- a/packages/wallet/backend/src/app.ts +++ b/packages/wallet/backend/src/app.ts @@ -322,6 +322,11 @@ export class App { ) router.get('/cards/:cardId/pin', isAuth, cardController.getPin) router.post('/cards/:cardId/change-pin', isAuth, cardController.changePin) + router.put( + '/cards/:cardId/block', + isAuth, + cardController.permanentlyBlockCard + ) // Return an error for invalid routes router.use('*', (req: Request, res: CustomResponse) => { diff --git a/packages/wallet/backend/src/card/controller.ts b/packages/wallet/backend/src/card/controller.ts index 81dc70113..17b70ada7 100644 --- a/packages/wallet/backend/src/card/controller.ts +++ b/packages/wallet/backend/src/card/controller.ts @@ -17,7 +17,8 @@ import { lockCardSchema, unlockCardSchema, getCardTransactionsSchema, - changePinSchema + changePinSchema, + permanentlyBlockCardSchema } from './validation' export interface ICardController { @@ -28,6 +29,7 @@ export interface ICardController { changePin: Controller lock: Controller unlock: Controller + permanentlyBlockCard: Controller } export class CardController implements ICardController { @@ -130,12 +132,14 @@ export class CardController implements ICardController { public lock = async (req: Request, res: Response, next: NextFunction) => { try { + const userId = req.session.user.id const { params, query, body } = await validate(lockCardSchema, req) const { cardId } = params const { reasonCode } = query const requestBody: ICardLockRequest = body const result = await this.cardService.lock( + userId, cardId, reasonCode, requestBody @@ -149,15 +153,38 @@ export class CardController implements ICardController { public unlock = async (req: Request, res: Response, next: NextFunction) => { try { + const userId = req.session.user.id const { params, body } = await validate(unlockCardSchema, req) const { cardId } = params const requestBody: ICardUnlockRequest = body - const result = await this.cardService.unlock(cardId, requestBody) + const result = await this.cardService.unlock(userId, cardId, requestBody) res.status(200).json(toSuccessResponse(result)) } catch (error) { next(error) } } + + public permanentlyBlockCard = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const userId = req.session.user.id + const { params, query } = await validate(permanentlyBlockCardSchema, req) + const { cardId } = params + const { reasonCode } = query + + const result = await this.cardService.permanentlyBlockCard( + userId, + cardId, + reasonCode + ) + res.status(200).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 3cf4550a9..a3a72e60c 100644 --- a/packages/wallet/backend/src/card/service.ts +++ b/packages/wallet/backend/src/card/service.ts @@ -10,6 +10,7 @@ import { import { IGetTransactionsResponse } from '@wallet/shared/src' import { LockReasonCode } from '@wallet/shared/src' import { NotFound } from '@shared/backend' +import { BlockReasonCode } from '@wallet/shared/src' export class CardService { constructor( @@ -27,7 +28,6 @@ export class CardService { ): Promise { const { cardId } = requestBody await this.ensureWalletAddressExists(userId, cardId) - await this.ensureWalletAddressExists(userId, cardId) return this.gateHubClient.getCardDetails(requestBody) } @@ -64,20 +64,36 @@ export class CardService { } async lock( + userId: string, cardId: string, reasonCode: LockReasonCode, requestBody: ICardLockRequest ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + return this.gateHubClient.lockCard(cardId, reasonCode, requestBody) } async unlock( + userId: string, cardId: string, requestBody: ICardUnlockRequest ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + return this.gateHubClient.unlockCard(cardId, requestBody) } + async permanentlyBlockCard( + userId: string, + cardId: string, + reasonCode: BlockReasonCode + ): Promise { + await this.ensureWalletAddressExists(userId, cardId) + + return this.gateHubClient.permanentlyBlockCard(cardId, reasonCode) + } + private async ensureWalletAddressExists( userId: string, cardId: string diff --git a/packages/wallet/backend/src/card/validation.ts b/packages/wallet/backend/src/card/validation.ts index a27ae2e69..869e1d3e6 100644 --- a/packages/wallet/backend/src/card/validation.ts +++ b/packages/wallet/backend/src/card/validation.ts @@ -61,3 +61,23 @@ export const changePinSchema = z.object({ cypher: z.string() }) }) + +export const permanentlyBlockCardSchema = z.object({ + params: z.object({ + cardId: z.string() + }), + query: z.object({ + reasonCode: z.enum([ + 'LostCard', + 'StolenCard', + 'IssuerRequestGeneral', + 'IssuerRequestFraud', + 'IssuerRequestLegal', + 'IssuerRequestIncorrectOpening', + 'CardDamagedOrNotWorking', + 'UserRequest', + 'IssuerRequestCustomerDeceased', + 'ProductDoesNotRenew' + ]) + }) +}) diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index 5952efc38..d1d21f2c9 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -47,6 +47,8 @@ import { ICardLockRequest, ICardUnlockRequest } from '@/card/types' +import { BlockReasonCode } from '@wallet/shared/src' + export class GateHubClient { private clientIds = SANDBOX_CLIENT_IDS private mainUrl = 'sandbox.gatehub.net' @@ -403,40 +405,6 @@ export class GateHubClient { return this.request('GET', url) } - async lockCard( - cardId: string, - reasonCode: LockReasonCode, - requestBody: ICardLockRequest - ): Promise { - let url = `${this.apiUrl}/v1/cards/${cardId}/lock` - url += `?reasonCode=${encodeURIComponent(reasonCode)}` - - return this.request( - 'PUT', - url, - JSON.stringify(requestBody), - { - cardAppId: this.env.GATEHUB_CARD_APP_ID - } - ) - } - - async unlockCard( - cardId: string, - requestBody: ICardUnlockRequest - ): Promise { - const url = `${this.apiUrl}/v1/cards/${cardId}/unlock` - - return this.request( - 'PUT', - url, - JSON.stringify(requestBody), - { - cardAppId: this.env.GATEHUB_CARD_APP_ID - } - ) - } - async getPin( requestBody: ICardDetailsRequest ): Promise { @@ -501,6 +469,51 @@ export class GateHubClient { ) } + async lockCard( + cardId: string, + reasonCode: LockReasonCode, + requestBody: ICardLockRequest + ): Promise { + let url = `${this.apiUrl}/v1/cards/${cardId}/lock` + url += `?reasonCode=${encodeURIComponent(reasonCode)}` + + return this.request( + 'PUT', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + } + + async unlockCard( + cardId: string, + requestBody: ICardUnlockRequest + ): Promise { + const url = `${this.apiUrl}/v1/cards/${cardId}/unlock` + + return this.request( + 'PUT', + url, + JSON.stringify(requestBody), + { + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + } + + async permanentlyBlockCard( + cardId: string, + reasonCode: BlockReasonCode + ): Promise { + let url = `${this.apiUrl}/v1/cards/${cardId}/block` + + url += `?reasonCode=${encodeURIComponent(reasonCode)}` + + return this.request('PUT', url) + } + private async request( method: HTTP_METHODS, url: string, diff --git a/packages/wallet/backend/tests/cards/controller.test.ts b/packages/wallet/backend/tests/cards/controller.test.ts index 92f985d98..f2e35b8ca 100644 --- a/packages/wallet/backend/tests/cards/controller.test.ts +++ b/packages/wallet/backend/tests/cards/controller.test.ts @@ -40,7 +40,8 @@ describe('CardController', () => { lock: jest.fn(), unlock: jest.fn(), getPin: jest.fn(), - changePin: jest.fn() + changePin: jest.fn(), + permanentlyBlockCard: jest.fn() } const args = mockLogInRequest().body @@ -337,6 +338,7 @@ describe('CardController', () => { await cardController.lock(req, res, next) expect(mockCardService.lock).toHaveBeenCalledWith( + userId, 'test-card-id', 'LostCard', { @@ -434,9 +436,13 @@ describe('CardController', () => { await cardController.unlock(req, res, next) - expect(mockCardService.unlock).toHaveBeenCalledWith('test-card-id', { - note: 'Found my card' - }) + expect(mockCardService.unlock).toHaveBeenCalledWith( + userId, + 'test-card-id', + { + note: 'Found my card' + } + ) expect(res.statusCode).toBe(200) expect(res._getJSONData()).toEqual({ @@ -603,4 +609,53 @@ describe('CardController', () => { expect(res.statusCode).toBe(400) }) }) + + describe('permanentlyBlockCard', () => { + it('should get block card successfully', async () => { + const next = jest.fn() + + mockCardService.permanentlyBlockCard.mockResolvedValue({}) + + req.params = { cardId: 'test-card-id' } + req.query = { reasonCode: 'StolenCard' } + + await cardController.permanentlyBlockCard(req, res, next) + + expect(mockCardService.permanentlyBlockCard).toHaveBeenCalledWith( + userId, + 'test-card-id', + 'StolenCard' + ) + expect(res.statusCode).toBe(200) + expect(res._getJSONData()).toEqual({ + success: true, + message: 'SUCCESS', + result: {} + }) + }) + it('should return 400 if reasonCode is invalid', async () => { + const next = jest.fn() + + req.params = { cardId: 'test-card-id' } + req.query = { reasonCode: 'InvalidCode' } + + await cardController.permanentlyBlockCard(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) + expect(res._getJSONData()).toEqual({ + success: false, + message: 'Invalid input' + }) + }) + }) }) diff --git a/packages/wallet/shared/src/types/card.ts b/packages/wallet/shared/src/types/card.ts index 68d6fcc06..10af60c56 100644 --- a/packages/wallet/shared/src/types/card.ts +++ b/packages/wallet/shared/src/types/card.ts @@ -6,6 +6,18 @@ export type LockReasonCode = | 'IssuerRequestFraud' | 'IssuerRequestLegal' +export type BlockReasonCode = + | 'LostCard' + | 'StolenCard' + | 'IssuerRequestGeneral' + | 'IssuerRequestFraud' + | 'IssuerRequestLegal' + | 'IssuerRequestIncorrectOpening' + | 'CardDamagedOrNotWorking' + | 'UserRequest' + | 'IssuerRequestCustomerDeceased' + | 'ProductDoesNotRenew' + // Response for fetching card transactions export interface ITransaction { id: number