Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallet/backend): add view card details endpoint #1633

Merged
merged 13 commits into from
Sep 26, 2024
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
Loading