diff --git a/docker/dev/.env.example b/docker/dev/.env.example index b7310d192..18a2fd310 100644 --- a/docker/dev/.env.example +++ b/docker/dev/.env.example @@ -14,6 +14,10 @@ GATEHUB_VAULT_UUID_EUR= GATEHUB_VAULT_UUID_USD= GATEHUB_SETTLEMENT_WALLET_ADDRESS= GATEHUB_CARD_APP_ID= +GATEHUB_ACCOUNT_PRODUCT_CODE= +GATEHUB_CARD_PRODUCT_CODE= +GATEHUB_NAME_ON_CARD= +GATEHUB_CARD_PP_PREFIX= # commerce env variables # encoded base 64 private key diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index 96c71df94..87213603f 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -58,6 +58,10 @@ services: GATEHUB_VAULT_UUID_USD: ${GATEHUB_VAULT_UUID_USD} GATEHUB_SETTLEMENT_WALLET_ADDRESS: ${GATEHUB_SETTLEMENT_WALLET_ADDRESS} GATEHUB_CARD_APP_ID: ${GATEHUB_CARD_APP_ID} + GATEHUB_ACCOUNT_PRODUCT_CODE: ${GATEHUB_ACCOUNT_PRODUCT_CODE} + GATEHUB_CARD_PRODUCT_CODE: ${GATEHUB_CARD_PRODUCT_CODE} + GATEHUB_NAME_ON_CARD: ${GATEHUB_NAME_ON_CARD} + GATEHUB_CARD_PP_PREFIX: ${GATEHUB_CARD_PP_PREFIX} restart: always networks: - testnet diff --git a/docker/prod/.env.example b/docker/prod/.env.example index b0c85e84f..3944dcd66 100644 --- a/docker/prod/.env.example +++ b/docker/prod/.env.example @@ -35,6 +35,10 @@ WALLET_BACKEND_GATEHUB_VAULT_UUID_EUR= WALLET_BACKEND_GATEHUB_VAULT_UUID_USD= WALLET_BACKEND_GATEHUB_SETTLEMENT_WALLET_ADDRESS= WALLET_BACKEND_GATEHUB_CARD_APP_ID= +WALLET_BACKEND_GATEHUB_ACCOUNT_PRODUCT_CODE= +WALLET_BACKEND_GATEHUB_CARD_PRODUCT_CODE= +WALLET_BACKEND_GATEHUB_NAME_ON_CARD= +WALLET_BACKEND_GATEHUB_CARD_PP_PREFIX= # BOUTIQUE BOUTIQUE_BACKEND_PORT= diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 7f89a2dcf..444c03e84 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -70,6 +70,10 @@ services: 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} + GATEHUB_ACCOUNT_PRODUCT_CODE: ${WALLET_BACKEND_GATEHUB_ACCOUNT_PRODUCT_CODE} + GATEHUB_CARD_PRODUCT_CODE: ${WALLET_BACKEND_GATEHUB_CARD_PRODUCT_CODE} + GATEHUB_NAME_ON_CARD: ${WALLET_BACKEND_GATEHUB_NAME_ON_CARD} + GATEHUB_CARD_PP_PREFIX: ${WALLET_BACKEND_GATEHUB_CARD_PP_PREFIX} networks: - testnet ports: diff --git a/packages/wallet/backend/migrations/20241008143459_add_customer_id_to_users.js b/packages/wallet/backend/migrations/20241008143459_add_customer_id_to_users.js new file mode 100644 index 000000000..ae1b26719 --- /dev/null +++ b/packages/wallet/backend/migrations/20241008143459_add_customer_id_to_users.js @@ -0,0 +1,19 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.table('users', function (table) { + table.string('customerId').unique().nullable() + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.table('users', function (table) { + table.dropColumn('customerId') + }) +} diff --git a/packages/wallet/backend/src/card/types.ts b/packages/wallet/backend/src/card/types.ts index e9663e9e5..80fa83c1a 100644 --- a/packages/wallet/backend/src/card/types.ts +++ b/packages/wallet/backend/src/card/types.ts @@ -1,5 +1,7 @@ import { CardLimitType } from '@wallet/shared/src' +export type GateHubCardCurrency = 'EUR' + export interface ICardDetailsRequest { cardId: string publicKeyBase64: string @@ -19,39 +21,125 @@ export interface ILinksResponse { } export interface ICreateCustomerRequest { - emailAddress: string + walletAddress: string account: { productCode: string + currency: GateHubCardCurrency + card: { + 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 + nameOnCard: string + citizen: { + name: string + surname: string + birthPlace?: string | null } } +export interface ICitizen { + name: string + surname: string + birthDate?: string | null + birthPlace?: string | null + gender?: 'Female' | 'Male' | 'Unspecified' | 'Unknown' | null + title?: string | null + language?: string | null +} + +export interface ILegalEntity { + longName: string + shortName: string + sector?: + | 'Public' + | 'Private' + | 'Corporate' + | 'Others' + | 'NoInformation' + | 'UnrelatedPersonsLegalEntities' + | null + industrialClassificationProvider?: string | null + industrialClassificationValue?: string | null + type?: string | null + vat?: string | null + hqCustomerId?: number | null + contactPerson?: string | null + agentCode?: string | null + agentName?: string | null +} + +export interface IAddress { + sourceId?: string | null + type: 'PermanentResidence' | 'Work' | 'Other' | 'TemporaryResidence' + countryCode: string + line1: string + line2?: string | null + line3?: string | null + city: string + postOffice?: string | null + zipCode: string + status?: 'Inactive' | 'Active' | null + id?: string | null + customerId?: string | null + customerSourceId?: string | null +} + +export interface ICommunication { + sourceId?: string | null + type: 'Email' | 'Mobile' + value?: string | null + id?: string | null + status?: 'Inactive' | 'Active' | null + customerId?: string | null + customerSourceId?: string | null +} + +export interface IAccount { + sourceId?: string | null + type?: 'CHARGE' | 'LOAN' | 'DEBIT' | 'PREPAID' | null + productCode?: string | null + accountNumber?: string | null + feeProfile?: string | null + accountProfile?: string | null + id?: string | null + customerId?: string | null + customerSourceId?: string | null + status?: 'ACTIVE' | 'LOCKED' | 'BLOCKED' | null + statusReasonCode?: + | 'TemporaryBlockForDelinquency' + | 'TemporaryBlockOnIssuerRequest' + | 'TemporaryBlockForDepo' + | 'TemporaryBlockForAmlKyc' + | 'IssuerRequestGeneral' + | 'UserRequest' + | 'PremanentBlockChargeOff' + | 'IssuerRequestBureauInquiry' + | 'IssuerRequestCustomerDeceased' + | 'IssuerRequestStornoFromCollectionStraight' + | 'IssuerRequestStornoFromCollectionDepo' + | 'IssuerRequestStornoFromCollectionDepoPaid' + | 'IssuerRequestHandoverToAttorney' + | 'IssuerRequestLegalAction' + | 'IssuerRequestAmlKyc' + | null + currency?: string | null + cards?: ICardResponse[] | null +} + export interface ICreateCustomerResponse { - customerId: string - accountId: string - cardId: string + walletAddress: string + customers: { + sourceId?: string | null + taxNumber?: string | null + code: string + type: 'Citizen' | 'LegalEntity' + citizen?: ICitizen | null + legalEntity?: ILegalEntity | null + id?: string | null + addresses?: IAddress[] | null + communications?: ICommunication[] | null + accounts?: IAccount[] | null + } } export interface ICardResponse { @@ -108,3 +196,22 @@ export interface ICardLimitResponse { currency: string isDisabled: boolean } + +export type CloseCardReason = + | 'IssuerRequestGeneral' + | 'IssuerRequestFraud' + | 'IssuerRequestLegal' + | 'IssuerRequestIncorrectOpening' + | 'UserRequest' + | 'IssuerRequestCustomerDeceased' + +export interface ICreateCardRequest { + nameOnCard: string + deliveryAddressId: string + walletAddress: string + currency: GateHubCardCurrency + productCode: string + card: { + productCode: string + } +} diff --git a/packages/wallet/backend/src/cardUsers.ts b/packages/wallet/backend/src/cardUsers.ts new file mode 100644 index 000000000..f1a8832bd --- /dev/null +++ b/packages/wallet/backend/src/cardUsers.ts @@ -0,0 +1,111 @@ +import { createContainer } from '@/createContainer' +import { env } from '@/config/env' +import { ICreateCustomerRequest } from '@/card/types' + +interface UserData { + email: string + firstName: string + lastName: string + ppNumber: string +} + +const entries = `John;Doe;john@doe.com;8888 +Alice;Smith;alice@smith.com;9999` + +function processEntries() { + const users: Array = [] + const lines = entries.split('\n') + for (const line of lines) { + const [firstName, lastName, email, ppNumber] = line.split(';') + users.push({ + email, + firstName, + lastName, + ppNumber + }) + } + + return users +} + +async function cardManagement() { + const container = await createContainer(env) + const logger = container.resolve('logger') + const knex = container.resolve('knex') + const gateHubClient = container.resolve('gateHubClient') + const accountProductCode = env.GATEHUB_ACCOUNT_PRODUCT_CODE + const cardProductCode = env.GATEHUB_CARD_PRODUCT_CODE + const nameOnCard = env.GATEHUB_NAME_ON_CARD + const ppPrefix = env.GATEHUB_CARD_PP_PREFIX + const GATEWAY_UUID = env.GATEHUB_GATEWAY_UUID + + try { + const usersData = processEntries() + + for (const userData of usersData) { + const { email, firstName, lastName, ppNumber } = userData + + const managedUser = await gateHubClient.createManagedUser(email) + + logger.info(`Created managed user for ${email}: ${managedUser.id}`) + + await gateHubClient.connectUserToGateway(managedUser.id, GATEWAY_UUID) + + logger.info( + `Connected user ${managedUser.id} - ${managedUser.email} to gateway` + ) + + const user = await gateHubClient.getWalletForUser(managedUser.id) + const walletAddress = user.wallets[0].address + + logger.info(`Retrieved user ${managedUser.id} wallet - ${walletAddress}`) + + // Create customer using product codes as env vars + const createCustomerRequestBody: ICreateCustomerRequest = { + walletAddress, + account: { + productCode: accountProductCode, + currency: 'EUR', + card: { + productCode: cardProductCode + } + }, + nameOnCard, + citizen: { + name: firstName, + surname: lastName + } + } + + const customer = await gateHubClient.createCustomer( + managedUser.id, + createCustomerRequestBody + ) + + logger.info(`Created customer for ${email}: ${customer.customers.id}`) + + const pp = ppPrefix + ppNumber + await gateHubClient.updateMetaForManagedUser(managedUser.id, { + paymentPointer: pp.toLowerCase(), + customerId: customer.customers.id! + }) + + logger.info(`Updated meta object for user ${email}`) + + const accounts = customer.customers.accounts![0] + const card = accounts.cards![0] + + await gateHubClient.orderPlasticForCard(managedUser.id, card.id) + } + } catch (error: unknown) { + console.log(`An error occurred: ${(error as Error).message}`) + } finally { + await knex.destroy() + await container.dispose() + } +} + +cardManagement().catch((error) => { + console.error(`Script failed: ${error.message}`) + process.exit(1) +}) diff --git a/packages/wallet/backend/src/config/env.ts b/packages/wallet/backend/src/config/env.ts index 433d49cbd..c53b8583c 100644 --- a/packages/wallet/backend/src/config/env.ts +++ b/packages/wallet/backend/src/config/env.ts @@ -23,6 +23,15 @@ const envSchema = z.object({ .string() .default('GATEHUB_SETTLEMENT_WALLET_ADDRESS'), GATEHUB_CARD_APP_ID: z.string().default('GATEHUB_CARD_APP_ID'), + GATEHUB_ACCOUNT_PRODUCT_CODE: z + .string() + .default('GATEHUB_ACCOUNT_PRODUCT_CODE'), + GATEHUB_CARD_PRODUCT_CODE: z.string().default('GATEHUB_CARD_PRODUCT_CODE'), + GATEHUB_NAME_ON_CARD: z + .string() + .regex(/^[a-zA-Z0-9]*$/, 'Only alphanumeric characters are allowed') + .default('INTERLEDGER'), + GATEHUB_CARD_PP_PREFIX: z.string().default('GATEHUB_GATEHUB_CARD_PP_PREFIX'), GRAPHQL_ENDPOINT: z.string().url().default('http://localhost:3011/graphql'), AUTH_GRAPHQL_ENDPOINT: z .string() diff --git a/packages/wallet/backend/src/gatehub/client.ts b/packages/wallet/backend/src/gatehub/client.ts index 957708c46..e033c63f4 100644 --- a/packages/wallet/backend/src/gatehub/client.ts +++ b/packages/wallet/backend/src/gatehub/client.ts @@ -13,6 +13,7 @@ import { IFundAccountRequest, IGetUserStateResponse, IGetVaultsResponse, + IGetWalletForUserResponse, IGetWalletResponse, IRatesResponse, ITokenRequest, @@ -48,7 +49,9 @@ import { ICardLockRequest, ICardUnlockRequest, ICardLimitResponse, - ICardLimitRequest + ICardLimitRequest, + ICreateCardRequest, + CloseCardReason } from '@/card/types' import { BlockReasonCode } from '@wallet/shared/src' @@ -181,6 +184,24 @@ export class GateHubClient { return response } + /** + * The meta was createad as `meta.meta.[property]` + * We should be aware of this when the user signs up (for production) + */ + async updateMetaForManagedUser( + userUuid: string, + meta: Record + ): Promise { + const url = `${this.apiUrl}/auth/v1/users/managed` + // This is the reason why the `meta` was created as `meta.meta`. + // Keeping this as is for consistency + const body = { meta } + + return await this.request('PUT', url, JSON.stringify(body), { + managedUserUuid: userUuid + }) + } + async createManagedUser(email: string): Promise { const url = `${this.apiUrl}/auth/v1/users/managed` const body: ICreateManagedUserRequest = { email } @@ -275,6 +296,26 @@ export class GateHubClient { return response } + /** + * Retrieves the user with its corresponding wallets. + * + * !!! The `meta` object is not present here - not the same output as + * ICreateManagedUserResponse !!! + */ + async getWalletForUser(userUuid: string): Promise { + const url = `${this.apiUrl}/core/v1/users/${userUuid}` + + const response = await this.request( + 'GET', + url, + undefined, + { + managedUserUuid: userUuid + } + ) + return response + } + async getWalletBalance( walletId: string, managedUserUuid: string @@ -338,17 +379,31 @@ export class GateHubClient { } async createCustomer( + userUuid: string, requestBody: ICreateCustomerRequest ): Promise { - const url = `${this.apiUrl}/v1/customers` - return this.request( + const url = `${this.apiUrl}/cards/v1/customers/managed` + const response = await this.request( 'POST', url, JSON.stringify(requestBody), { + managedUserUuid: userUuid, cardAppId: this.env.GATEHUB_CARD_APP_ID } ) + return response + } + + /** + * @deprecated Only used when ordering cards. + */ + async orderPlasticForCard(userUuid: string, cardId: string): Promise { + const url = `${this.apiUrl}/cards/v1/cards/${cardId}/plastic` + await this.request('POST', url, undefined, { + managedUserUuid: userUuid, + cardAppId: this.env.GATEHUB_CARD_APP_ID + }) } async getCardsByCustomer(customerId: string): Promise { @@ -546,6 +601,38 @@ export class GateHubClient { return this.request('PUT', url) } + async closeCard(userUuid: string, cardId: string, reason: CloseCardReason) { + const url = `${this.apiUrl}/cards/v1/cards/${cardId}/card?reasonCode=${reason}` + + await this.request('DELETE', url, undefined, { + managedUserUuid: userUuid, + cardAppId: this.env.GATEHUB_CARD_APP_ID + }) + } + + /** + * @deprecated + */ + async createCard( + userUuid: string, + accountId: string, + payload: ICreateCardRequest + ) { + const url = `${this.apiUrl}/cards/v1/cards/${accountId}/card` + + const response = await this.request( + 'POST', + url, + JSON.stringify(payload), + { + managedUserUuid: userUuid, + cardAppId: this.env.GATEHUB_CARD_APP_ID + } + ) + + return response + } + private async request( method: HTTP_METHODS, url: string, diff --git a/packages/wallet/backend/src/gatehub/types.ts b/packages/wallet/backend/src/gatehub/types.ts index 1c1fa5865..6d15a5a2d 100644 --- a/packages/wallet/backend/src/gatehub/types.ts +++ b/packages/wallet/backend/src/gatehub/types.ts @@ -31,7 +31,12 @@ export interface ICreateManagedUserResponse { type2fa: string activated: boolean role: string - meta: Record + meta: { + meta: { + paymentPointer: string + customerId: string + } + } & Record lastPasswordChange: string features: string[] managed: boolean @@ -57,6 +62,11 @@ export interface ICreateWalletRequest { export interface ICreateWalletResponse { address: string } + +export interface IGetWalletForUserResponse { + wallets: ICreateWalletResponse[] +} + export interface IGetWalletResponse { address: string } diff --git a/packages/wallet/backend/src/reorder.ts b/packages/wallet/backend/src/reorder.ts new file mode 100644 index 000000000..631e9dba8 --- /dev/null +++ b/packages/wallet/backend/src/reorder.ts @@ -0,0 +1,97 @@ +import { createContainer } from '@/createContainer' +import { env } from '@/config/env' + +interface Entry { + userUuid: string + accountId: string + cardId: string + customerId: string + walletAddress: string + email: string + deliveryAddressId: string +} + +const entries = `userUuid1,accountId1,cardId1,customerId1,walletAddress1,john@doe.com,deliveryAddressId1 +userUuid2,accountId2,cardId2,customerId2,walletAddress2,alice@smith.com,deliveryAddressId2` + +function processEntries() { + const e: Array = [] + const lines = entries.split('\n') + for (const line of lines) { + const [ + userUuid, + accountId, + cardId, + customerId, + walletAddress, + email, + deliveryAddressId + ] = line.split(',') + e.push({ + userUuid, + accountId, + cardId, + customerId, + walletAddress, + email, + deliveryAddressId + }) + } + + return e +} + +async function reorder() { + const container = await createContainer(env) + const logger = container.resolve('logger') + const knex = container.resolve('knex') + const gateHubClient = container.resolve('gateHubClient') + const accountProductCode = env.GATEHUB_ACCOUNT_PRODUCT_CODE + const cardProductCode = env.GATEHUB_CARD_PRODUCT_CODE + + try { + const entries = processEntries() + + for (const entry of entries) { + const { userUuid, accountId, cardId, walletAddress, deliveryAddressId } = + entry + + await gateHubClient.closeCard( + userUuid, + cardId, + 'IssuerRequestIncorrectOpening' + ) + + logger.info(`Closed card with cardId: ${cardId}; user: ${userUuid}`) + + const card = await gateHubClient.createCard(userUuid, accountId, { + nameOnCard: 'INTERLEDGER', + deliveryAddressId, + walletAddress, + currency: 'EUR', + productCode: accountProductCode, + card: { productCode: cardProductCode } + }) + + logger.info( + `Created card with cardId: ${card.id}; customerId: ${card.customerId}` + ) + + await gateHubClient.orderPlasticForCard(userUuid, card.id) + + logger.info( + `Ordered plastic card for user: ${userUuid}; new card id: ${card.id}` + ) + } + } catch (error: unknown) { + console.log(`An error occurred: ${(error as Error).message}`) + } finally { + await knex.destroy() + await container.dispose() + } +} + +reorder().catch((error) => { + console.error(`Script failed: ${error.message}`) + process.exit(1) +}) diff --git a/packages/wallet/backend/src/user/model.ts b/packages/wallet/backend/src/user/model.ts index c853b307b..b70239124 100644 --- a/packages/wallet/backend/src/user/model.ts +++ b/packages/wallet/backend/src/user/model.ts @@ -20,6 +20,7 @@ export class User extends BaseModel { public kycVerified!: boolean public gateHubUserId?: string + public customerId?: string public sessions?: Session[] public passwordResetToken?: string | null