Skip to content

Commit 314bc80

Browse files
authored
feat(wallet/backend): add permanently block card endpoint (#1662)
* Add permanently block card endpoint * Prettier * Add controller tests * Rearrange functions * Ensure WA exists for lock and unlock calls * Fix backend tests
1 parent c784e41 commit 314bc80

File tree

7 files changed

+189
-41
lines changed

7 files changed

+189
-41
lines changed

packages/wallet/backend/src/app.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,11 @@ export class App {
322322
)
323323
router.get('/cards/:cardId/pin', isAuth, cardController.getPin)
324324
router.post('/cards/:cardId/change-pin', isAuth, cardController.changePin)
325+
router.put(
326+
'/cards/:cardId/block',
327+
isAuth,
328+
cardController.permanentlyBlockCard
329+
)
325330

326331
// Return an error for invalid routes
327332
router.use('*', (req: Request, res: CustomResponse) => {

packages/wallet/backend/src/card/controller.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
lockCardSchema,
1818
unlockCardSchema,
1919
getCardTransactionsSchema,
20-
changePinSchema
20+
changePinSchema,
21+
permanentlyBlockCardSchema
2122
} from './validation'
2223

2324
export interface ICardController {
@@ -28,6 +29,7 @@ export interface ICardController {
2829
changePin: Controller<void>
2930
lock: Controller<ICardResponse>
3031
unlock: Controller<ICardResponse>
32+
permanentlyBlockCard: Controller<ICardResponse>
3133
}
3234

3335
export class CardController implements ICardController {
@@ -130,12 +132,14 @@ export class CardController implements ICardController {
130132

131133
public lock = async (req: Request, res: Response, next: NextFunction) => {
132134
try {
135+
const userId = req.session.user.id
133136
const { params, query, body } = await validate(lockCardSchema, req)
134137
const { cardId } = params
135138
const { reasonCode } = query
136139
const requestBody: ICardLockRequest = body
137140

138141
const result = await this.cardService.lock(
142+
userId,
139143
cardId,
140144
reasonCode,
141145
requestBody
@@ -149,15 +153,38 @@ export class CardController implements ICardController {
149153

150154
public unlock = async (req: Request, res: Response, next: NextFunction) => {
151155
try {
156+
const userId = req.session.user.id
152157
const { params, body } = await validate(unlockCardSchema, req)
153158
const { cardId } = params
154159
const requestBody: ICardUnlockRequest = body
155160

156-
const result = await this.cardService.unlock(cardId, requestBody)
161+
const result = await this.cardService.unlock(userId, cardId, requestBody)
157162

158163
res.status(200).json(toSuccessResponse(result))
159164
} catch (error) {
160165
next(error)
161166
}
162167
}
168+
169+
public permanentlyBlockCard = async (
170+
req: Request,
171+
res: Response,
172+
next: NextFunction
173+
) => {
174+
try {
175+
const userId = req.session.user.id
176+
const { params, query } = await validate(permanentlyBlockCardSchema, req)
177+
const { cardId } = params
178+
const { reasonCode } = query
179+
180+
const result = await this.cardService.permanentlyBlockCard(
181+
userId,
182+
cardId,
183+
reasonCode
184+
)
185+
res.status(200).json(toSuccessResponse(result))
186+
} catch (error) {
187+
next(error)
188+
}
189+
}
163190
}

packages/wallet/backend/src/card/service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { IGetTransactionsResponse } from '@wallet/shared/src'
1111
import { LockReasonCode } from '@wallet/shared/src'
1212
import { NotFound } from '@shared/backend'
13+
import { BlockReasonCode } from '@wallet/shared/src'
1314

1415
export class CardService {
1516
constructor(
@@ -27,7 +28,6 @@ export class CardService {
2728
): Promise<ICardDetailsResponse> {
2829
const { cardId } = requestBody
2930
await this.ensureWalletAddressExists(userId, cardId)
30-
await this.ensureWalletAddressExists(userId, cardId)
3131

3232
return this.gateHubClient.getCardDetails(requestBody)
3333
}
@@ -64,20 +64,36 @@ export class CardService {
6464
}
6565

6666
async lock(
67+
userId: string,
6768
cardId: string,
6869
reasonCode: LockReasonCode,
6970
requestBody: ICardLockRequest
7071
): Promise<ICardResponse> {
72+
await this.ensureWalletAddressExists(userId, cardId)
73+
7174
return this.gateHubClient.lockCard(cardId, reasonCode, requestBody)
7275
}
7376

7477
async unlock(
78+
userId: string,
7579
cardId: string,
7680
requestBody: ICardUnlockRequest
7781
): Promise<ICardResponse> {
82+
await this.ensureWalletAddressExists(userId, cardId)
83+
7884
return this.gateHubClient.unlockCard(cardId, requestBody)
7985
}
8086

87+
async permanentlyBlockCard(
88+
userId: string,
89+
cardId: string,
90+
reasonCode: BlockReasonCode
91+
): Promise<ICardResponse> {
92+
await this.ensureWalletAddressExists(userId, cardId)
93+
94+
return this.gateHubClient.permanentlyBlockCard(cardId, reasonCode)
95+
}
96+
8197
private async ensureWalletAddressExists(
8298
userId: string,
8399
cardId: string

packages/wallet/backend/src/card/validation.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,23 @@ export const changePinSchema = z.object({
6161
cypher: z.string()
6262
})
6363
})
64+
65+
export const permanentlyBlockCardSchema = z.object({
66+
params: z.object({
67+
cardId: z.string()
68+
}),
69+
query: z.object({
70+
reasonCode: z.enum([
71+
'LostCard',
72+
'StolenCard',
73+
'IssuerRequestGeneral',
74+
'IssuerRequestFraud',
75+
'IssuerRequestLegal',
76+
'IssuerRequestIncorrectOpening',
77+
'CardDamagedOrNotWorking',
78+
'UserRequest',
79+
'IssuerRequestCustomerDeceased',
80+
'ProductDoesNotRenew'
81+
])
82+
})
83+
})

packages/wallet/backend/src/gatehub/client.ts

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import {
4747
ICardLockRequest,
4848
ICardUnlockRequest
4949
} from '@/card/types'
50+
import { BlockReasonCode } from '@wallet/shared/src'
51+
5052
export class GateHubClient {
5153
private clientIds = SANDBOX_CLIENT_IDS
5254
private mainUrl = 'sandbox.gatehub.net'
@@ -403,40 +405,6 @@ export class GateHubClient {
403405
return this.request<IGetTransactionsResponse>('GET', url)
404406
}
405407

406-
async lockCard(
407-
cardId: string,
408-
reasonCode: LockReasonCode,
409-
requestBody: ICardLockRequest
410-
): Promise<ICardResponse> {
411-
let url = `${this.apiUrl}/v1/cards/${cardId}/lock`
412-
url += `?reasonCode=${encodeURIComponent(reasonCode)}`
413-
414-
return this.request<ICardResponse>(
415-
'PUT',
416-
url,
417-
JSON.stringify(requestBody),
418-
{
419-
cardAppId: this.env.GATEHUB_CARD_APP_ID
420-
}
421-
)
422-
}
423-
424-
async unlockCard(
425-
cardId: string,
426-
requestBody: ICardUnlockRequest
427-
): Promise<ICardResponse> {
428-
const url = `${this.apiUrl}/v1/cards/${cardId}/unlock`
429-
430-
return this.request<ICardResponse>(
431-
'PUT',
432-
url,
433-
JSON.stringify(requestBody),
434-
{
435-
cardAppId: this.env.GATEHUB_CARD_APP_ID
436-
}
437-
)
438-
}
439-
440408
async getPin(
441409
requestBody: ICardDetailsRequest
442410
): Promise<ICardDetailsResponse> {
@@ -501,6 +469,51 @@ export class GateHubClient {
501469
)
502470
}
503471

472+
async lockCard(
473+
cardId: string,
474+
reasonCode: LockReasonCode,
475+
requestBody: ICardLockRequest
476+
): Promise<ICardResponse> {
477+
let url = `${this.apiUrl}/v1/cards/${cardId}/lock`
478+
url += `?reasonCode=${encodeURIComponent(reasonCode)}`
479+
480+
return this.request<ICardResponse>(
481+
'PUT',
482+
url,
483+
JSON.stringify(requestBody),
484+
{
485+
cardAppId: this.env.GATEHUB_CARD_APP_ID
486+
}
487+
)
488+
}
489+
490+
async unlockCard(
491+
cardId: string,
492+
requestBody: ICardUnlockRequest
493+
): Promise<ICardResponse> {
494+
const url = `${this.apiUrl}/v1/cards/${cardId}/unlock`
495+
496+
return this.request<ICardResponse>(
497+
'PUT',
498+
url,
499+
JSON.stringify(requestBody),
500+
{
501+
cardAppId: this.env.GATEHUB_CARD_APP_ID
502+
}
503+
)
504+
}
505+
506+
async permanentlyBlockCard(
507+
cardId: string,
508+
reasonCode: BlockReasonCode
509+
): Promise<ICardResponse> {
510+
let url = `${this.apiUrl}/v1/cards/${cardId}/block`
511+
512+
url += `?reasonCode=${encodeURIComponent(reasonCode)}`
513+
514+
return this.request<ICardResponse>('PUT', url)
515+
}
516+
504517
private async request<T>(
505518
method: HTTP_METHODS,
506519
url: string,

packages/wallet/backend/tests/cards/controller.test.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ describe('CardController', () => {
4040
lock: jest.fn(),
4141
unlock: jest.fn(),
4242
getPin: jest.fn(),
43-
changePin: jest.fn()
43+
changePin: jest.fn(),
44+
permanentlyBlockCard: jest.fn()
4445
}
4546

4647
const args = mockLogInRequest().body
@@ -337,6 +338,7 @@ describe('CardController', () => {
337338
await cardController.lock(req, res, next)
338339

339340
expect(mockCardService.lock).toHaveBeenCalledWith(
341+
userId,
340342
'test-card-id',
341343
'LostCard',
342344
{
@@ -434,9 +436,13 @@ describe('CardController', () => {
434436

435437
await cardController.unlock(req, res, next)
436438

437-
expect(mockCardService.unlock).toHaveBeenCalledWith('test-card-id', {
438-
note: 'Found my card'
439-
})
439+
expect(mockCardService.unlock).toHaveBeenCalledWith(
440+
userId,
441+
'test-card-id',
442+
{
443+
note: 'Found my card'
444+
}
445+
)
440446

441447
expect(res.statusCode).toBe(200)
442448
expect(res._getJSONData()).toEqual({
@@ -603,4 +609,53 @@ describe('CardController', () => {
603609
expect(res.statusCode).toBe(400)
604610
})
605611
})
612+
613+
describe('permanentlyBlockCard', () => {
614+
it('should get block card successfully', async () => {
615+
const next = jest.fn()
616+
617+
mockCardService.permanentlyBlockCard.mockResolvedValue({})
618+
619+
req.params = { cardId: 'test-card-id' }
620+
req.query = { reasonCode: 'StolenCard' }
621+
622+
await cardController.permanentlyBlockCard(req, res, next)
623+
624+
expect(mockCardService.permanentlyBlockCard).toHaveBeenCalledWith(
625+
userId,
626+
'test-card-id',
627+
'StolenCard'
628+
)
629+
expect(res.statusCode).toBe(200)
630+
expect(res._getJSONData()).toEqual({
631+
success: true,
632+
message: 'SUCCESS',
633+
result: {}
634+
})
635+
})
636+
it('should return 400 if reasonCode is invalid', async () => {
637+
const next = jest.fn()
638+
639+
req.params = { cardId: 'test-card-id' }
640+
req.query = { reasonCode: 'InvalidCode' }
641+
642+
await cardController.permanentlyBlockCard(req, res, (err) => {
643+
next(err)
644+
res.status(err.statusCode).json({
645+
success: false,
646+
message: err.message
647+
})
648+
})
649+
650+
expect(next).toHaveBeenCalled()
651+
const error = next.mock.calls[0][0]
652+
expect(error).toBeInstanceOf(BadRequest)
653+
expect(error.message).toBe('Invalid input')
654+
expect(res.statusCode).toBe(400)
655+
expect(res._getJSONData()).toEqual({
656+
success: false,
657+
message: 'Invalid input'
658+
})
659+
})
660+
})
606661
})

packages/wallet/shared/src/types/card.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ export type LockReasonCode =
66
| 'IssuerRequestFraud'
77
| 'IssuerRequestLegal'
88

9+
export type BlockReasonCode =
10+
| 'LostCard'
11+
| 'StolenCard'
12+
| 'IssuerRequestGeneral'
13+
| 'IssuerRequestFraud'
14+
| 'IssuerRequestLegal'
15+
| 'IssuerRequestIncorrectOpening'
16+
| 'CardDamagedOrNotWorking'
17+
| 'UserRequest'
18+
| 'IssuerRequestCustomerDeceased'
19+
| 'ProductDoesNotRenew'
20+
921
// Response for fetching card transactions
1022
export interface ITransaction {
1123
id: number

0 commit comments

Comments
 (0)