Skip to content

Commit

Permalink
feat(wallet/backend): add view card details endpoint (#1633)
Browse files Browse the repository at this point in the history
* Add view card details endpoint

* Prettier

* Fix backend tests

* Add endpoint for masked details

* Small refactor

* Prettier

* Add migrations and check for WA mapping to card

* Add tests

* Update endpoints

* Update getCardDetails request with URL

* Address comments

* Remove duplicate card app id header
  • Loading branch information
sanducb authored Sep 26, 2024
1 parent 68698e3 commit 9bb15a9
Show file tree
Hide file tree
Showing 16 changed files with 583 additions and 30 deletions.
1 change: 1 addition & 0 deletions docker/dev/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ GATEHUB_GATEWAY_UUID=
GATEHUB_VAULT_UUID_EUR=
GATEHUB_VAULT_UUID_USD=
GATEHUB_SETTLEMENT_WALLET_ADDRESS=
GATEHUB_CARD_APP_ID=

# commerce env variables
# encoded base 64 private key
Expand Down
1 change: 1 addition & 0 deletions docker/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ services:
GATEHUB_VAULT_UUID_EUR: ${GATEHUB_VAULT_UUID_EUR}
GATEHUB_VAULT_UUID_USD: ${GATEHUB_VAULT_UUID_USD}
GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${GATEHUB_SETTLEMENT_WALLET_ADDRESS}
GATEHUB_CARD_APP_ID: ${GATEHUB_CARD_APP_ID}
restart: always
networks:
- testnet
Expand Down
1 change: 1 addition & 0 deletions docker/prod/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ WALLET_BACKEND_GATEHUB_GATEWAY_UUID=
WALLET_BACKEND_GATEHUB_VAULT_UUID_EUR=
WALLET_BACKEND_GATEHUB_VAULT_UUID_USD=
WALLET_BACKEND_GATEHUB_SETTLEMENT_WALLET_ADDRESS=
WALLET_BACKEND_GATEHUB_CARD_APP_ID=

# BOUTIQUE
BOUTIQUE_BACKEND_PORT=
Expand Down
1 change: 1 addition & 0 deletions docker/prod/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ services:
GATEHUB_VAULT_UUID_EUR: ${WALLET_BACKEND_GATEHUB_VAULT_UUID_EUR}
GATEHUB_VAULT_UUID_USD: ${WALLET_BACKEND_GATEHUB_VAULT_UUID_USD}
GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${WALLET_BACKEND_GATEHUB_SETTLEMENT_WALLET_ADDRESS}
GATEHUB_CARD_APP_ID: ${WALLET_BACKEND_GATEHUB_CARD_APP_ID}
networks:
- testnet
ports:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('walletAddresses', function (table) {
table.string('cardId').unique().nullable()
})
}

exports.down = function (knex) {
return knex.schema.table('walletAddresses', function (table) {
table.dropColumn('cardId')
})
}
13 changes: 13 additions & 0 deletions packages/wallet/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { initErrorHandler, RedisClient } from '@shared/backend'
import { GateHubController } from '@/gatehub/controller'
import { GateHubClient } from '@/gatehub/client'
import { GateHubService } from '@/gatehub/service'
import { CardController } from './card/controller'
import { CardService } from './card/service'

export interface Bindings {
env: Env
Expand Down Expand Up @@ -77,6 +79,8 @@ export interface Bindings {
gateHubClient: GateHubClient
gateHubController: GateHubController
gateHubService: GateHubService
cardService: CardService
cardController: CardController
}

export class App {
Expand Down Expand Up @@ -145,6 +149,7 @@ export class App {
const accountController = await this.container.resolve('accountController')
const rafikiController = await this.container.resolve('rafikiController')
const gateHubController = await this.container.resolve('gateHubController')
const cardController = await this.container.resolve('cardController')

app.use(
cors({
Expand Down Expand Up @@ -301,6 +306,14 @@ export class App {
gateHubController.addUserToGateway
)

// Cards
router.get(
'/customers/:customerId/cards',
isAuth,
cardController.getCardsByCustomer
)
router.get('/cards/:cardId/details', isAuth, cardController.getCardDetails)

// Return an error for invalid routes
router.use('*', (req: Request, res: CustomResponse) => {
const e = Error(`Requested path ${req.path} was not found`)
Expand Down
58 changes: 58 additions & 0 deletions packages/wallet/backend/src/card/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Request, Response, NextFunction } from 'express'
import { Controller } from '@shared/backend'
import { CardService } from '@/card/service'
import { toSuccessResponse } from '@shared/backend'
import {
ICardDetailsRequest,
ICardDetailsResponse,
ICardResponse
} from './types'
import { validate } from '@/shared/validate'
import { getCardsByCustomerSchema, getCardDetailsSchema } from './validation'

export interface ICardController {
getCardsByCustomer: Controller<ICardDetailsResponse[]>
getCardDetails: Controller<ICardResponse>
}

export class CardController implements ICardController {
constructor(private cardService: CardService) {}

public getCardsByCustomer = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const { params } = await validate(getCardsByCustomerSchema, req)
const { customerId } = params

const cards = await this.cardService.getCardsByCustomer(customerId)
res.status(200).json(toSuccessResponse(cards))
} catch (error) {
next(error)
}
}

public getCardDetails = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const userId = req.session.user.id
const { params, body } = await validate(getCardDetailsSchema, req)
const { cardId } = params
const { publicKeyBase64 } = body

const requestBody: ICardDetailsRequest = { cardId, publicKeyBase64 }
const cardDetails = await this.cardService.getCardDetails(
userId,
requestBody
)
res.status(200).json(toSuccessResponse(cardDetails))
} catch (error) {
next(error)
}
}
}
36 changes: 36 additions & 0 deletions packages/wallet/backend/src/card/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { WalletAddressService } from '@/walletAddress/service'
import { GateHubClient } from '../gatehub/client'
import {
ICardDetailsRequest,
ICardDetailsResponse,
ICardResponse
} from './types'
import { NotFound } from '@shared/backend'

export class CardService {
constructor(
private gateHubClient: GateHubClient,
private walletAddressService: WalletAddressService
) {}

async getCardsByCustomer(customerId: string): Promise<ICardResponse[]> {
return this.gateHubClient.getCardsByCustomer(customerId)
}

async getCardDetails(
userId: string,
requestBody: ICardDetailsRequest
): Promise<ICardDetailsResponse> {
const { cardId } = requestBody

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.getCardDetails(requestBody)
}
}
96 changes: 96 additions & 0 deletions packages/wallet/backend/src/card/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
export interface ICardDetailsRequest {
cardId: string
publicKeyBase64: string
}

export interface ICardDetailsResponse {
cipher: string | null
}

export interface ILinksResponse {
token: string | null
links: Array<{
href: string | null
rel: string | null
method: string | null
}> | null
}

export interface ICreateCustomerRequest {
emailAddress: string
account: {
productCode: string
}
card: {
productCode: string
}
user: {
firstName: string
lastName: string
mobileNumber?: string
nationalIdentifier?: string
}
identification: {
documents: Array<{
type: string
file: string // Base64-encoded file content
}>
}
address: {
addressLine1: string
addressLine2?: string
city: string
region?: string
postalCode: string
countryCode: string
}
}

export interface ICreateCustomerResponse {
customerId: string
accountId: string
cardId: string
}

export interface ICardResponse {
sourceId: string
nameOnCard: string
productCode: string
id: string
accountId: string
accountSourceId: string
maskedPan: string
status: string
statusReasonCode: string | null
lockLevel: string | null
expiryDate: string
customerId: string
customerSourceId: string
}

export type CardLimitType =
| 'perTransaction'
| 'dailyOverall'
| 'weeklyOverall'
| 'monthlyOverall'
| 'dailyAtm'
| 'dailyEcomm'
| 'monthlyOpenScheme'
| 'nonEUPayments'

export interface ICardProductLimit {
type: CardLimitType
currency: string
limit: string
isDisabled: boolean
}

export interface ICardProductResponse {
cardProductLimits: ICardProductLimit[]
deletedAt: string | null
uuid: string
accountProductCode: string
code: string
name: string
cost: string
}
16 changes: 16 additions & 0 deletions packages/wallet/backend/src/card/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod'

export const getCardsByCustomerSchema = z.object({
params: z.object({
customerId: z.string()
})
})

export const getCardDetailsSchema = z.object({
params: z.object({
cardId: z.string()
}),
body: z.object({
publicKeyBase64: z.string()
})
})
1 change: 1 addition & 0 deletions packages/wallet/backend/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const envSchema = z.object({
GATEHUB_SETTLEMENT_WALLET_ADDRESS: z
.string()
.default('GATEHUB_SETTLEMENT_WALLET_ADDRESS'),
GATEHUB_CARD_APP_ID: z.string().default('GATEHUB_CARD_APP_ID'),
GRAPHQL_ENDPOINT: z.string().url().default('http://localhost:3011/graphql'),
AUTH_GRAPHQL_ENDPOINT: z
.string()
Expand Down
8 changes: 7 additions & 1 deletion packages/wallet/backend/src/createContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import { KratosService } from './rafiki/kratos.service'
import { GateHubController } from '@/gatehub/controller'
import { GateHubClient } from '@/gatehub/client'
import { GateHubService } from '@/gatehub/service'
import { CardController } from './card/controller'
import { CardService } from './card/service'

export interface Cradle {
env: Env
Expand Down Expand Up @@ -92,6 +94,8 @@ export interface Cradle {
gateHubClient: GateHubClient
gateHubController: GateHubController
gateHubService: GateHubService
cardService: CardService
cardController: CardController
}

export async function createContainer(
Expand Down Expand Up @@ -129,6 +133,7 @@ export async function createContainer(
quoteService: asClass(QuoteService).singleton(),
grantService: asClass(GrantService).singleton(),
socketService: asClassSingletonWithLogger(SocketService, logger),
cardService: asClass(CardService).singleton(),
userController: asClass(UserController).singleton(),
authController: asClass(AuthController).singleton(),
assetController: asClass(AssetController).singleton(),
Expand All @@ -144,7 +149,8 @@ export async function createContainer(
kratosService: asClassSingletonWithLogger(KratosService, logger),
gateHubClient: asClass(GateHubClient).singleton(),
gateHubController: asClass(GateHubController).singleton(),
gateHubService: asClass(GateHubService).singleton()
gateHubService: asClass(GateHubService).singleton(),
cardController: asClass(CardController).singleton()
})

return container
Expand Down
Loading

0 comments on commit 9bb15a9

Please sign in to comment.