From c643d5b62596dd15c6e4e3537a6a8e3934005027 Mon Sep 17 00:00:00 2001 From: bsanduc Date: Tue, 1 Oct 2024 10:44:46 +0300 Subject: [PATCH 1/6] Add permanently block card endpoint --- packages/wallet/backend/src/app.ts | 5 ++++ .../wallet/backend/src/card/controller.ts | 28 ++++++++++++++++++- packages/wallet/backend/src/card/service.ts | 17 +++++++++++ .../wallet/backend/src/card/validation.ts | 20 +++++++++++++ packages/wallet/backend/src/gatehub/client.ts | 12 ++++++++ packages/wallet/shared/src/types/card.ts | 11 ++++++++ packages/wallet/shared/src/types/index.ts | 1 + 7 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 packages/wallet/shared/src/types/card.ts diff --git a/packages/wallet/backend/src/app.ts b/packages/wallet/backend/src/app.ts index 8aaf62825..6b73a1d23 100644 --- a/packages/wallet/backend/src/app.ts +++ b/packages/wallet/backend/src/app.ts @@ -313,6 +313,11 @@ export class App { cardController.getCardsByCustomer ) router.get('/cards/:cardId/details', isAuth, cardController.getCardDetails) + 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 581c3f469..82038b786 100644 --- a/packages/wallet/backend/src/card/controller.ts +++ b/packages/wallet/backend/src/card/controller.ts @@ -8,7 +8,11 @@ import { ICardResponse } from './types' import { validate } from '@/shared/validate' -import { getCardsByCustomerSchema, getCardDetailsSchema } from './validation' +import { + getCardsByCustomerSchema, + getCardDetailsSchema, + permanentlyBlockCardSchema +} from './validation' export interface ICardController { getCardsByCustomer: Controller @@ -55,4 +59,26 @@ export class CardController implements ICardController { 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 bd2a023b2..a91bd4447 100644 --- a/packages/wallet/backend/src/card/service.ts +++ b/packages/wallet/backend/src/card/service.ts @@ -6,6 +6,7 @@ import { ICardResponse } from './types' import { NotFound } from '@shared/backend' +import { BlockReasonCode } from '@wallet/shared/src' export class CardService { constructor( @@ -33,4 +34,20 @@ export class CardService { return this.gateHubClient.getCardDetails(requestBody) } + + async permanentlyBlockCard( + userId: string, + cardId: string, + reasonCode: BlockReasonCode + ): Promise { + const walletAddress = await this.walletAddressService.getByCardId( + userId, + cardId + ) + if (!walletAddress) { + throw new NotFound('Card not found or not associated with the user.') + } + + return this.gateHubClient.permanentlyBlockCard(cardId, reasonCode) + } } diff --git a/packages/wallet/backend/src/card/validation.ts b/packages/wallet/backend/src/card/validation.ts index d43f84b2d..0083a863d 100644 --- a/packages/wallet/backend/src/card/validation.ts +++ b/packages/wallet/backend/src/card/validation.ts @@ -14,3 +14,23 @@ export const getCardDetailsSchema = z.object({ publicKeyBase64: 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 469eb6f28..5bb7642c1 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -41,6 +41,7 @@ import { ICardProductResponse, ICardDetailsRequest } from '@/card/types' +import { BlockReasonCode } from '@wallet/shared/src' export class GateHubClient { private clientIds = SANDBOX_CLIENT_IDS @@ -371,6 +372,17 @@ export class GateHubClient { return cardDetailsResponse } + 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/shared/src/types/card.ts b/packages/wallet/shared/src/types/card.ts new file mode 100644 index 000000000..db21274bf --- /dev/null +++ b/packages/wallet/shared/src/types/card.ts @@ -0,0 +1,11 @@ +export type BlockReasonCode = + | 'LostCard' + | 'StolenCard' + | 'IssuerRequestGeneral' + | 'IssuerRequestFraud' + | 'IssuerRequestLegal' + | 'IssuerRequestIncorrectOpening' + | 'CardDamagedOrNotWorking' + | 'UserRequest' + | 'IssuerRequestCustomerDeceased' + | 'ProductDoesNotRenew'; \ No newline at end of file diff --git a/packages/wallet/shared/src/types/index.ts b/packages/wallet/shared/src/types/index.ts index d8baf80ee..ad25b2eb9 100644 --- a/packages/wallet/shared/src/types/index.ts +++ b/packages/wallet/shared/src/types/index.ts @@ -8,3 +8,4 @@ export * from './grant' export * from './walletAddress' export * from './user' export * from './iframe' +export * from './card' From 41d2f7006e74cbe3c7ac22891ab8948f5818188a Mon Sep 17 00:00:00 2001 From: bsanduc Date: Tue, 1 Oct 2024 11:07:15 +0300 Subject: [PATCH 2/6] Prettier --- packages/wallet/backend/src/card/controller.ts | 1 + packages/wallet/shared/src/types/card.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/wallet/backend/src/card/controller.ts b/packages/wallet/backend/src/card/controller.ts index da1673da5..6885baa25 100644 --- a/packages/wallet/backend/src/card/controller.ts +++ b/packages/wallet/backend/src/card/controller.ts @@ -23,6 +23,7 @@ export interface ICardController { getCardDetails: Controller lock: Controller unlock: Controller + permanentlyBlockCard: Controller } export class CardController implements ICardController { diff --git a/packages/wallet/shared/src/types/card.ts b/packages/wallet/shared/src/types/card.ts index 1091da539..4b47814f6 100644 --- a/packages/wallet/shared/src/types/card.ts +++ b/packages/wallet/shared/src/types/card.ts @@ -16,4 +16,4 @@ export type BlockReasonCode = | 'CardDamagedOrNotWorking' | 'UserRequest' | 'IssuerRequestCustomerDeceased' - | 'ProductDoesNotRenew'; \ No newline at end of file + | 'ProductDoesNotRenew' From 1b8709cb374c3ac94937f6878e4197e95e688ef7 Mon Sep 17 00:00:00 2001 From: bsanduc Date: Tue, 1 Oct 2024 16:47:49 +0300 Subject: [PATCH 3/6] Add controller tests --- .../backend/tests/cards/controller.test.ts | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/wallet/backend/tests/cards/controller.test.ts b/packages/wallet/backend/tests/cards/controller.test.ts index 04694de65..e678ec7d4 100644 --- a/packages/wallet/backend/tests/cards/controller.test.ts +++ b/packages/wallet/backend/tests/cards/controller.test.ts @@ -36,7 +36,8 @@ describe('CardController', () => { getCardsByCustomer: jest.fn(), getCardDetails: jest.fn(), lock: jest.fn(), - unlock: jest.fn() + unlock: jest.fn(), + permanentlyBlockCard: jest.fn() } const args = mockLogInRequest().body @@ -332,4 +333,53 @@ describe('CardController', () => { }) }) }) + + 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' + }) + }) + }) }) From fcfc76bdb3b4677a4f03088c2591b4ea62f7bb82 Mon Sep 17 00:00:00 2001 From: bsanduc Date: Wed, 2 Oct 2024 15:24:26 +0300 Subject: [PATCH 4/6] Rearrange functions --- packages/wallet/backend/src/gatehub/client.ts | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index fb59f1474..d1d21f2c9 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -405,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 { @@ -503,6 +469,40 @@ 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 From b0de9ce911c28f6cfaba534003ca02d225978f7a Mon Sep 17 00:00:00 2001 From: bsanduc Date: Wed, 2 Oct 2024 15:26:35 +0300 Subject: [PATCH 5/6] Ensure WA exists for lock and unlock calls --- packages/wallet/backend/src/card/controller.ts | 5 ++++- packages/wallet/backend/src/card/service.ts | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/wallet/backend/src/card/controller.ts b/packages/wallet/backend/src/card/controller.ts index 43dbac018..17b70ada7 100644 --- a/packages/wallet/backend/src/card/controller.ts +++ b/packages/wallet/backend/src/card/controller.ts @@ -132,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 @@ -151,11 +153,12 @@ 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) { diff --git a/packages/wallet/backend/src/card/service.ts b/packages/wallet/backend/src/card/service.ts index 16a460394..a3a72e60c 100644 --- a/packages/wallet/backend/src/card/service.ts +++ b/packages/wallet/backend/src/card/service.ts @@ -64,17 +64,23 @@ 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) } From e834d2ba462029dd209e763270c1c420101601b7 Mon Sep 17 00:00:00 2001 From: bsanduc Date: Wed, 2 Oct 2024 15:41:35 +0300 Subject: [PATCH 6/6] Fix backend tests --- .../wallet/backend/tests/cards/controller.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/wallet/backend/tests/cards/controller.test.ts b/packages/wallet/backend/tests/cards/controller.test.ts index 68cdb198c..f2e35b8ca 100644 --- a/packages/wallet/backend/tests/cards/controller.test.ts +++ b/packages/wallet/backend/tests/cards/controller.test.ts @@ -338,6 +338,7 @@ describe('CardController', () => { await cardController.lock(req, res, next) expect(mockCardService.lock).toHaveBeenCalledWith( + userId, 'test-card-id', 'LostCard', { @@ -435,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({