From bbe2fbc966408bf4f96c807c308e0d8dc3bf4509 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 09:34:31 +0530 Subject: [PATCH 1/9] feat: Provision mainnet account for customers on account bootstrap Signed-off-by: jay-dee7 --- .gitignore | 2 + src/controllers/api/account.ts | 98 +++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 1857e62a..0b74ab08 100644 --- a/.gitignore +++ b/.gitignore @@ -475,3 +475,5 @@ cython_debug/ #.idea/ *.tar +*.prod +*.local diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 4b8a8dce..ea8b469c 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -31,6 +31,8 @@ import { SafeAPIResponse } from '../../types/common.js'; import { RoleEntity } from '../../database/entities/role.entity.js'; import { getStripeObjectKey } from '../../utils/index.js'; import type { SupportedPlanTypes } from '../../types/admin.js'; +import { KeyService } from '../../services/api/key.js'; +import { KeyEntity } from '../../database/entities/key.entity.js'; dotenv.config(); export class AccountController { @@ -166,8 +168,6 @@ export class AccountController { // 9. Check the token balance for Testnet account // 10. Add the Stripe account to the Customer - let paymentAccount: PaymentAccountEntity | null; - // 1. Get logTo UserId from request body if (!request.body.user || !request.body.user.id || !request.body.user.primaryEmail) { return response.status(StatusCodes.BAD_REQUEST).json({ @@ -288,52 +288,112 @@ export class AccountController { // 6. Check is paymentAccount exists for the customer const accounts = await PaymentAccountService.instance.find({ customer: customerEntity }); - if (accounts.length === 0) { - const key = await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( + // const key = await (async () => { + // if (accounts.length === 0) { + // return await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( + // SupportedKeyTypes.Secp256k1, + // customerEntity + // ); + // } + // + // return await KeyService.instance.keyRepository.findOne({ where: { customer: customerEntity } }); + // })(); + // + // if (!key) { + // return response.status(StatusCodes.BAD_REQUEST).json({ + // error: 'PaymentAccount is not found in database: Key was not created', + // } satisfies UnsuccessfulResponseBody); + // } + + const existingMainnetAccount = accounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet); + const existingTestnetAccount = accounts.find((acc) => acc.namespace === CheqdNetwork.Testnet); + let mainnetKey: KeyEntity | null = null; + let testnetKey: KeyEntity | null = null; + + let mainnetAccount: PaymentAccountEntity | null; + let testnetAccount: PaymentAccountEntity | null; + + if (existingMainnetAccount) { + mainnetAccount = existingMainnetAccount; + mainnetKey = await KeyService.instance.get(existingMainnetAccount.key.kid); + } else { + mainnetKey = await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( SupportedKeyTypes.Secp256k1, customerEntity ); - if (!key) { + if (!mainnetKey) { return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in database: Key was not created', + error: 'PaymentAccount is not found in database: Mainnet key was not created', } satisfies UnsuccessfulResponseBody); } - paymentAccount = (await PaymentAccountService.instance.create( - CheqdNetwork.Testnet, + mainnetAccount = (await PaymentAccountService.instance.create( + CheqdNetwork.Mainnet, true, customerEntity, - key + mainnetKey )) as PaymentAccountEntity; - if (!paymentAccount) { + if (!mainnetAccount) { return response.status(StatusCodes.BAD_REQUEST).json({ error: 'PaymentAccount is not found in database: Payment account was not created', } satisfies UnsuccessfulResponseBody); } - // Notify await eventTracker.notify({ message: EventTracker.compileBasicNotification( 'PaymentAccount was not found in database: Payment account with address: ' + - paymentAccount.address + + mainnetAccount.address + ' was created' ), severity: 'info', }); + } + + if (existingTestnetAccount) { + testnetAccount = existingTestnetAccount; + testnetKey = await KeyService.instance.get(existingTestnetAccount.key.kid); } else { - paymentAccount = accounts[0]; + testnetKey = await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( + SupportedKeyTypes.Secp256k1, + customerEntity + ); + if (!testnetKey) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'PaymentAccount is not found in database: Testnet key was not created', + } satisfies UnsuccessfulResponseBody); + } + testnetAccount = (await PaymentAccountService.instance.create( + CheqdNetwork.Testnet, + true, + customerEntity, + testnetKey + )) as PaymentAccountEntity; + if (!testnetAccount) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'PaymentAccount is not found in database: Payment account was not created', + } satisfies UnsuccessfulResponseBody); + } + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + 'PaymentAccount was not found in database: Payment account with address: ' + + testnetAccount.address + + ' was created' + ), + severity: 'info', + }); } // 7. Assign default role on LogTo const customDataFromLogTo = await logToHelper.getCustomData(userEntity.logToId); // 8. Create custom_data and update the userInfo (send it to the LogTo) - if (Object.keys(customDataFromLogTo.data).length === 0 && paymentAccount.address) { + if (Object.keys(customDataFromLogTo.data).length === 0 && testnetAccount.address && mainnetAccount.address) { const customData = { customer: { id: customerEntity.customerId, name: customerEntity.name, }, paymentAccount: { - address: paymentAccount.address, + address: testnetAccount.address, + mainnet: mainnetAccount.address, }, }; const _r = await logToHelper.updateCustomData(userEntity.logToId, customData); @@ -345,12 +405,12 @@ export class AccountController { } // 9. Check the token balance for Testnet account - if (paymentAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') { - const balances = await checkBalance(paymentAccount.address, process.env.TESTNET_RPC_URL); + if (testnetAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') { + const balances = await checkBalance(testnetAccount.address, process.env.TESTNET_RPC_URL); const balance = balances[0]; if (!balance || +balance.amount < TESTNET_MINIMUM_BALANCE * Math.pow(10, DEFAULT_DENOM_EXPONENT)) { // 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet - const resp = await FaucetHelper.delegateTokens(paymentAccount.address); + const resp = await FaucetHelper.delegateTokens(testnetAccount.address); if (resp.status !== StatusCodes.OK) { return response.status(StatusCodes.BAD_GATEWAY).json({ error: resp.error, @@ -359,7 +419,7 @@ export class AccountController { // Notify await eventTracker.notify({ message: EventTracker.compileBasicNotification( - `Testnet account with address: ${paymentAccount.address} was funded with ${TESTNET_MINIMUM_BALANCE}` + `Testnet account with address: ${testnetAccount.address} was funded with ${TESTNET_MINIMUM_BALANCE}` ), severity: 'info', }); From 3f6775a3ae267e6022f3b7ac04c2216b7a2674e1 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 12:45:40 +0530 Subject: [PATCH 2/9] refactor: Split customer account provisioning to a method Signed-off-by: jay-dee7 --- src/controllers/api/account.ts | 178 ++++++++++++++++----------------- 1 file changed, 87 insertions(+), 91 deletions(-) diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index ea8b469c..49c18eb3 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -32,7 +32,6 @@ import { RoleEntity } from '../../database/entities/role.entity.js'; import { getStripeObjectKey } from '../../utils/index.js'; import type { SupportedPlanTypes } from '../../types/admin.js'; import { KeyService } from '../../services/api/key.js'; -import { KeyEntity } from '../../database/entities/key.entity.js'; dotenv.config(); export class AccountController { @@ -288,99 +287,32 @@ export class AccountController { // 6. Check is paymentAccount exists for the customer const accounts = await PaymentAccountService.instance.find({ customer: customerEntity }); - // const key = await (async () => { - // if (accounts.length === 0) { - // return await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( - // SupportedKeyTypes.Secp256k1, - // customerEntity - // ); - // } - // - // return await KeyService.instance.keyRepository.findOne({ where: { customer: customerEntity } }); - // })(); - // - // if (!key) { - // return response.status(StatusCodes.BAD_REQUEST).json({ - // error: 'PaymentAccount is not found in database: Key was not created', - // } satisfies UnsuccessfulResponseBody); - // } - - const existingMainnetAccount = accounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet); - const existingTestnetAccount = accounts.find((acc) => acc.namespace === CheqdNetwork.Testnet); - let mainnetKey: KeyEntity | null = null; - let testnetKey: KeyEntity | null = null; - - let mainnetAccount: PaymentAccountEntity | null; - let testnetAccount: PaymentAccountEntity | null; - - if (existingMainnetAccount) { - mainnetAccount = existingMainnetAccount; - mainnetKey = await KeyService.instance.get(existingMainnetAccount.key.kid); - } else { - mainnetKey = await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( - SupportedKeyTypes.Secp256k1, - customerEntity - ); - if (!mainnetKey) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in database: Mainnet key was not created', - } satisfies UnsuccessfulResponseBody); - } - mainnetAccount = (await PaymentAccountService.instance.create( - CheqdNetwork.Mainnet, - true, - customerEntity, - mainnetKey - )) as PaymentAccountEntity; - if (!mainnetAccount) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in database: Payment account was not created', - } satisfies UnsuccessfulResponseBody); - } - await eventTracker.notify({ - message: EventTracker.compileBasicNotification( - 'PaymentAccount was not found in database: Payment account with address: ' + - mainnetAccount.address + - ' was created' - ), - severity: 'info', - }); + + const mainnetAccountResponse = await this.provisionCustomerAccount( + CheqdNetwork.Mainnet, + accounts, + customerEntity + ); + if (!mainnetAccountResponse.success) { + return response.status(mainnetAccountResponse.status).json({ + error: mainnetAccountResponse.error, + } satisfies UnsuccessfulResponseBody); } - if (existingTestnetAccount) { - testnetAccount = existingTestnetAccount; - testnetKey = await KeyService.instance.get(existingTestnetAccount.key.kid); - } else { - testnetKey = await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( - SupportedKeyTypes.Secp256k1, - customerEntity - ); - if (!testnetKey) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in database: Testnet key was not created', - } satisfies UnsuccessfulResponseBody); - } - testnetAccount = (await PaymentAccountService.instance.create( - CheqdNetwork.Testnet, - true, - customerEntity, - testnetKey - )) as PaymentAccountEntity; - if (!testnetAccount) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in database: Payment account was not created', - } satisfies UnsuccessfulResponseBody); - } - await eventTracker.notify({ - message: EventTracker.compileBasicNotification( - 'PaymentAccount was not found in database: Payment account with address: ' + - testnetAccount.address + - ' was created' - ), - severity: 'info', - }); + const testnetAccountResponse = await this.provisionCustomerAccount( + CheqdNetwork.Testnet, + accounts, + customerEntity + ); + if (!testnetAccountResponse.success) { + return response.status(testnetAccountResponse.status).json({ + error: testnetAccountResponse.error, + } satisfies UnsuccessfulResponseBody); } + const mainnetAccount = mainnetAccountResponse.data; + const testnetAccount = testnetAccountResponse.data; + // 7. Assign default role on LogTo const customDataFromLogTo = await logToHelper.getCustomData(userEntity.logToId); @@ -392,10 +324,11 @@ export class AccountController { name: customerEntity.name, }, paymentAccount: { - address: testnetAccount.address, + testnet: testnetAccount.address, mainnet: mainnetAccount.address, }, }; + const _r = await logToHelper.updateCustomData(userEntity.logToId, customData); if (_r.status !== 200) { return response.status(_r.status).json({ @@ -441,6 +374,69 @@ export class AccountController { return response.status(StatusCodes.OK).json({}); } + public async provisionCustomerAccount( + network: CheqdNetwork, + accounts: PaymentAccountEntity[], + customerEntity: CustomerEntity + ): Promise> { + const existingAccount = accounts.find((acc) => acc.namespace === network); + if (existingAccount) { + const key = await KeyService.instance.get(existingAccount.key.kid); + if (key) { + return { + success: true, + status: 200, + data: existingAccount, + }; + } + + return { + success: false, + status: 412, // precondition + error: `Error: account key not found for kid: ${existingAccount.key.kid}`, + }; + } + + const key = await new IdentityServiceStrategySetup(customerEntity.customerId).agent.createKey( + SupportedKeyTypes.Secp256k1, + customerEntity + ); + if (!key) { + return { + success: false, + status: 400, + error: `PaymentAccount is not found in database: ${network} key was not created`, + }; + } + + const account = (await PaymentAccountService.instance.create( + network, + true, + customerEntity, + key + )) as PaymentAccountEntity; + if (!account) { + return { + success: false, + status: 400, + error: 'PaymentAccount is not found in database: Payment account was not created', + }; + } + + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + `PaymentAccount was not found in database: Payment account with address: ${account.address} on ${network} was created` + ), + severity: 'info', + }); + + return { + success: true, + status: 200, + data: account, + }; + } + /** * @openapi * From 37c1ea3b47d38dc66cc043bbb530486c84edf7b0 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 12:53:45 +0530 Subject: [PATCH 3/9] fix: Convert the provisionCustomerAccount to static Signed-off-by: jay-dee7 --- src/controllers/api/account.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 49c18eb3..4d237b35 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -288,7 +288,7 @@ export class AccountController { // 6. Check is paymentAccount exists for the customer const accounts = await PaymentAccountService.instance.find({ customer: customerEntity }); - const mainnetAccountResponse = await this.provisionCustomerAccount( + const mainnetAccountResponse = await AccountController.provisionCustomerAccount( CheqdNetwork.Mainnet, accounts, customerEntity @@ -299,7 +299,7 @@ export class AccountController { } satisfies UnsuccessfulResponseBody); } - const testnetAccountResponse = await this.provisionCustomerAccount( + const testnetAccountResponse = await AccountController.provisionCustomerAccount( CheqdNetwork.Testnet, accounts, customerEntity @@ -374,7 +374,7 @@ export class AccountController { return response.status(StatusCodes.OK).json({}); } - public async provisionCustomerAccount( + static async provisionCustomerAccount( network: CheqdNetwork, accounts: PaymentAccountEntity[], customerEntity: CustomerEntity From f1287b4483c7e70565adfe9d92f7ff5527ecda11 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 14:00:05 +0530 Subject: [PATCH 4/9] fix: Include payment accounts in `GET Account` API Signed-off-by: jay-dee7 --- src/controllers/api/account.ts | 9 +++++---- src/types/customer.ts | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 4d237b35..0d304a40 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -73,16 +73,17 @@ export class AccountController { error: 'Bad state cause there is no customer assigned to the user yet. Please contact administrator.', } satisfies UnsuccessfulQueryCustomerResponseBody); } - const paymentAccount = await PaymentAccountService.instance.find({ customer: response.locals.customer }); - const result = { + const paymentAccounts = await PaymentAccountService.instance.find({ customer: response.locals.customer }); + const result: QueryCustomerResponseBody = { customer: { customerId: response.locals.customer.customerId, name: response.locals.customer.name, }, paymentAccount: { - address: paymentAccount[0].address, + mainnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address ?? null, + testnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address ?? null, }, - } satisfies QueryCustomerResponseBody; + }; return response.status(StatusCodes.OK).json(result); } catch (error) { diff --git a/src/types/customer.ts b/src/types/customer.ts index b4d9fc2a..fe21fcac 100644 --- a/src/types/customer.ts +++ b/src/types/customer.ts @@ -1,3 +1,4 @@ +import { CheqdNetwork } from '@cheqd/sdk'; import type { CustomerEntity } from '../database/entities/customer.entity.js'; import type { UnsuccessfulQueryResponseBody } from './shared.js'; @@ -9,7 +10,8 @@ export type QueryCustomerResponseBody = { name: string; }; paymentAccount: { - address: string; + [CheqdNetwork.Testnet]: string | null; + [CheqdNetwork.Mainnet]: string | null; }; }; From 6765931ea410bb4e359d4aa017bb4c902a15f1ba Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 14:03:37 +0530 Subject: [PATCH 5/9] fix: Handle account provisioning on Account Create API Signed-off-by: jay-dee7 --- src/controllers/api/account.ts | 83 ++++++++++++++++------------------ 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 0d304a40..81247f05 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -478,59 +478,56 @@ export class AccountController { // 3. Check is paymentAccount exists for the customer // 3.1. If no - create it // 4. Check the token balance for Testnet account - let customer: CustomerEntity | null; - let paymentAccount: PaymentAccountEntity | null; // 1. Get logTo UserId from request body const { username } = request.body; try { // 2. Check if the customer exists - if (response.locals.customer) { - customer = response.locals.customer as CustomerEntity; - } else { - // 2.1 Create customer - customer = (await CustomerService.instance.create(username)) as CustomerEntity; - if (!customer) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'Customer creation failed', - }); - } + const customerEntity = response.locals.customer + ? (response.locals.customer as CustomerEntity) + : // 2.1 Create customer + ((await CustomerService.instance.create(username)) as CustomerEntity); + + if (!customerEntity) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'Customer creation failed', + }); } // 3. Check if paymentAccount exists for the customer - const accounts = await PaymentAccountService.instance.find({ customer }); - paymentAccount = accounts.find((account) => account.namespace === CheqdNetwork.Testnet) || null; - if (paymentAccount === null) { - const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey( - SupportedKeyTypes.Secp256k1, - customer - ); - if (!key) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in database: Key was not created', - }); - } - paymentAccount = (await PaymentAccountService.instance.create( - CheqdNetwork.Testnet, - true, - customer, - key - )) as PaymentAccountEntity; - if (!paymentAccount) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'PaymentAccount is not found in database: Payment account was not created', - }); - } + const accounts = await PaymentAccountService.instance.find({ customer: customerEntity }); + const mainnetAccountResponse = await AccountController.provisionCustomerAccount( + CheqdNetwork.Mainnet, + accounts, + customerEntity + ); + if (!mainnetAccountResponse.success) { + return response.status(mainnetAccountResponse.status).json({ + error: mainnetAccountResponse.error, + } satisfies UnsuccessfulResponseBody); + } + + const testnetAccountResponse = await AccountController.provisionCustomerAccount( + CheqdNetwork.Testnet, + accounts, + customerEntity + ); + if (!testnetAccountResponse.success) { + return response.status(testnetAccountResponse.status).json({ + error: testnetAccountResponse.error, + } satisfies UnsuccessfulResponseBody); } + const testnetAccount = testnetAccountResponse.data; + // 4. Check the token balance for Testnet account - if (paymentAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') { - const balances = await checkBalance(paymentAccount.address, process.env.TESTNET_RPC_URL); + if (testnetAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') { + const balances = await checkBalance(testnetAccount.address, process.env.TESTNET_RPC_URL); const balance = balances[0]; if (!balance || +balance.amount < TESTNET_MINIMUM_BALANCE * Math.pow(10, DEFAULT_DENOM_EXPONENT)) { // 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet - const resp = await FaucetHelper.delegateTokens(paymentAccount.address); + const resp = await FaucetHelper.delegateTokens(testnetAccount.address); if (resp.status !== StatusCodes.OK) { return response.status(StatusCodes.BAD_GATEWAY).json({ @@ -540,17 +537,17 @@ export class AccountController { } } // 5. Setup stripe account - if (process.env.STRIPE_ENABLED === 'true' && !customer.paymentProviderId) { + if (process.env.STRIPE_ENABLED === 'true' && !customerEntity.paymentProviderId) { eventTracker.submit({ operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { - name: customer.name, - email: customer.email, - customerId: customer.customerId, + name: customerEntity.name, + email: customerEntity.email, + customerId: customerEntity.customerId, } satisfies ISubmitStripeCustomerCreateData, } satisfies ISubmitOperation); } - return response.status(StatusCodes.CREATED).json(customer); + return response.status(StatusCodes.CREATED).json(customerEntity); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Internal Error: ${(error as Error)?.message || error}`, From 273ebb82a0d8794376400f5ca618d90a0f342042 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 14:30:15 +0530 Subject: [PATCH 6/9] refactor: Update APIs referencing customer accounts Signed-off-by: jay-dee7 --- src/controllers/admin/organisation.ts | 19 ++++++++++++++++--- src/controllers/api/account.ts | 6 +++--- src/static/swagger-admin.json | 13 +++++++++++-- src/static/swagger-api.json | 5 ++++- src/types/admin.ts | 6 +++++- src/types/customer.ts | 4 ++-- src/types/swagger-admin-types.ts | 10 ++++++++-- src/types/swagger-api-types.ts | 4 +++- 8 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organisation.ts index 58af8fd6..923cc37b 100644 --- a/src/controllers/admin/organisation.ts +++ b/src/controllers/admin/organisation.ts @@ -5,11 +5,13 @@ import { validate } from '../validator/decorator.js'; import { CustomerService } from '../../services/api/customer.js'; import { StatusCodes } from 'http-status-codes'; import type { + AdminOrganisationGetResponseBody, AdminOrganisationGetUnsuccessfulResponseBody, AdminOrganisationUpdateResponseBody, AdminOrganisationUpdateUnsuccessfulResponseBody, } from '../../types/admin.js'; import { PaymentAccountService } from '../../services/api/payment-account.js'; +import { CheqdNetwork } from '@cheqd/sdk'; dotenv.config(); @@ -83,7 +85,12 @@ export class OrganisationController { name: customer.name, email: customer.email, description: customer.description, - cosmosAddress: paymentAccount[0].address as string, + cosmosAddress: { + [CheqdNetwork.Testnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Testnet) + ?.address, + [CheqdNetwork.Mainnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Mainnet) + ?.address, + }, } satisfies AdminOrganisationUpdateResponseBody); } catch (error) { return response.status(500).json({ @@ -126,12 +133,18 @@ export class OrganisationController { error: 'Customer for current user was not found or did not setup properly. Please contact administrator.', } satisfies AdminOrganisationGetUnsuccessfulResponseBody); } + return response.status(StatusCodes.OK).json({ name: customer.name, email: customer.email, description: customer.description, - cosmosAddress: paymentAccount[0].address as string, - }); + cosmosAddress: { + [CheqdNetwork.Testnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Testnet) + ?.address, + [CheqdNetwork.Mainnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Mainnet) + ?.address, + }, + } satisfies AdminOrganisationGetResponseBody); } catch (error) { return response.status(500).json({ error: `Internal error: ${(error as Error)?.message || error}`, diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 81247f05..4dc4190b 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -80,8 +80,8 @@ export class AccountController { name: response.locals.customer.name, }, paymentAccount: { - mainnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address ?? null, - testnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address ?? null, + mainnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address ?? '', + testnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address ?? '', }, }; @@ -325,8 +325,8 @@ export class AccountController { name: customerEntity.name, }, paymentAccount: { - testnet: testnetAccount.address, mainnet: mainnetAccount.address, + testnet: testnetAccount.address, }, }; diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 8827ad87..534b4899 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -509,9 +509,18 @@ "default": null }, "cosmosAddress": { - "type": "string", + "type": "object", "description": "The cosmos address of the organisation", - "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" + "properties": { + "testnet": { + "type": "string", + "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" + }, + "mainnet": { + "type": "string", + "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" + } + } } } }, diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index 3aa895c5..0049aafe 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -1954,7 +1954,10 @@ "paymentAccount": { "type": "object", "properties": { - "address": { + "mainnet": { + "type": "string" + }, + "testnet": { "type": "string" } } diff --git a/src/types/admin.ts b/src/types/admin.ts index 0333668a..3b5cf3a0 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -1,5 +1,6 @@ import type Stripe from 'stripe'; import type { UnsuccessfulResponseBody } from './shared.js'; +import { CheqdNetwork } from '@cheqd/sdk'; export type ProductWithPrices = Stripe.Product & { prices?: Stripe.Price[]; @@ -159,7 +160,10 @@ export type AdminOrganisationResponseBody = { name: string; email?: string; description?: string; - cosmosAddress?: string; + cosmosAddress?: { + [CheqdNetwork.Testnet]?: string; + [CheqdNetwork.Mainnet]?: string; + }; }; export type AdminOrganisationGetResponseBody = AdminOrganisationResponseBody; diff --git a/src/types/customer.ts b/src/types/customer.ts index fe21fcac..bc6211c6 100644 --- a/src/types/customer.ts +++ b/src/types/customer.ts @@ -10,8 +10,8 @@ export type QueryCustomerResponseBody = { name: string; }; paymentAccount: { - [CheqdNetwork.Testnet]: string | null; - [CheqdNetwork.Mainnet]: string | null; + [CheqdNetwork.Testnet]: string; + [CheqdNetwork.Mainnet]: string; }; }; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 61deec71..040e746f 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -340,9 +340,15 @@ * nullable: true * default: null * cosmosAddress: - * type: string + * type: object * description: The cosmos address of the organisation - * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u + * properties: + * testnet: + * type: string + * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u + * mainnet: + * type: string + * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. * type: object diff --git a/src/types/swagger-api-types.ts b/src/types/swagger-api-types.ts index 39ac0a61..ee888ed9 100644 --- a/src/types/swagger-api-types.ts +++ b/src/types/swagger-api-types.ts @@ -1285,7 +1285,9 @@ * paymentAccount: * type: object * properties: - * address: + * mainnet: + * type: string + * testnet: * type: string * QueryIdTokenResponseBody: * type: object From 87f82d1a42bfb9f6d8eec5920339056f9086c20f Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 15:22:36 +0530 Subject: [PATCH 7/9] fix: Await on event submit calls and check before creating stripe customer Signed-off-by: jay-dee7 --- src/controllers/api/account.ts | 6 +-- src/services/track/admin/account-submitter.ts | 52 +++++++++++-------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 4dc4190b..d2a2a074 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -275,7 +275,7 @@ export class AccountController { customerEntity = userEntity.customer; // this time user exists, so notify stripe account should be created. if (process.env.STRIPE_ENABLED === 'true' && !customerEntity.paymentProviderId) { - eventTracker.submit({ + await eventTracker.submit({ operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { name: customerEntity.name, @@ -362,7 +362,7 @@ export class AccountController { // 10. Add the Stripe account to the Customer if (process.env.STRIPE_ENABLED === 'true' && !customerEntity.paymentProviderId) { - eventTracker.submit({ + await eventTracker.submit({ operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { name: customerEntity.name, @@ -538,7 +538,7 @@ export class AccountController { } // 5. Setup stripe account if (process.env.STRIPE_ENABLED === 'true' && !customerEntity.paymentProviderId) { - eventTracker.submit({ + await eventTracker.submit({ operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { name: customerEntity.name, diff --git a/src/services/track/admin/account-submitter.ts b/src/services/track/admin/account-submitter.ts index a75e8bcc..87cc544a 100644 --- a/src/services/track/admin/account-submitter.ts +++ b/src/services/track/admin/account-submitter.ts @@ -29,35 +29,43 @@ export class PortalAccountCreateSubmitter implements IObserver { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); try { - // Create a new Stripe account - const account = await stripe.customers.create({ - name: data.name, - email: data.email, + const customer = await CustomerService.instance.customerRepository.findOne({ + where: { + email: data.email, + }, }); - if (account.lastResponse.statusCode !== StatusCodes.OK) { + + let stripeCustomer: Stripe.Response; + if (customer && !customer.paymentProviderId) { + // Create a new Stripe account + stripeCustomer = await stripe.customers.create({ + name: data.name, + email: data.email, + }); + if (stripeCustomer.lastResponse.statusCode !== StatusCodes.OK) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Failed to create Stripe account with name: ${data.name}.`, + operation.operation + ), + severity: 'error', + } as INotifyMessage); + return; + } this.notify({ message: EventTracker.compileBasicNotification( - `Failed to create Stripe account with name: ${data.name}.`, + `Stripe account created with name: ${data.name}.`, operation.operation ), - severity: 'error', + severity: 'info', } as INotifyMessage); - return; + // Update the CaaS customer with the new Stripe account. Note, we're populating the "name" field from stripe's response. + await CustomerService.instance.update({ + customerId: data.customerId, + name: data.name, + paymentProviderId: stripeCustomer.id, + }); } - - // Update the CaaS customer with the new Stripe account. Note, we're populating the "name" field from stripe's response. - await CustomerService.instance.update({ - customerId: data.customerId, - name: data.name, - paymentProviderId: account.id, - }); - this.notify({ - message: EventTracker.compileBasicNotification( - `Stripe account created with name: ${data.name}.`, - operation.operation - ), - severity: 'info', - } as INotifyMessage); } catch (error) { this.notify({ message: EventTracker.compileBasicNotification( From b4a314cee491f71723f446c315cfadf8e280200a Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 15:49:36 +0530 Subject: [PATCH 8/9] fix: Use null for missing accounts instead of undefined Signed-off-by: jay-dee7 --- src/controllers/admin/organisation.ts | 18 ++++++++++-------- src/controllers/api/account.ts | 4 ++-- src/static/swagger-admin.json | 4 +++- src/types/admin.ts | 4 ++-- src/types/customer.ts | 4 ++-- src/types/swagger-admin-types.ts | 2 ++ 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/controllers/admin/organisation.ts b/src/controllers/admin/organisation.ts index 923cc37b..2fa4e07e 100644 --- a/src/controllers/admin/organisation.ts +++ b/src/controllers/admin/organisation.ts @@ -81,15 +81,16 @@ export class OrganisationController { } satisfies AdminOrganisationUpdateUnsuccessfulResponseBody); } + const testnetAddress = paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address; + const mainnetAddress = paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address; + return response.status(StatusCodes.OK).json({ name: customer.name, email: customer.email, description: customer.description, cosmosAddress: { - [CheqdNetwork.Testnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Testnet) - ?.address, - [CheqdNetwork.Mainnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Mainnet) - ?.address, + [CheqdNetwork.Testnet]: testnetAddress ?? null, + [CheqdNetwork.Mainnet]: mainnetAddress ?? null, }, } satisfies AdminOrganisationUpdateResponseBody); } catch (error) { @@ -134,15 +135,16 @@ export class OrganisationController { } satisfies AdminOrganisationGetUnsuccessfulResponseBody); } + const testnetAddress = paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address; + const mainnetAddress = paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address; + return response.status(StatusCodes.OK).json({ name: customer.name, email: customer.email, description: customer.description, cosmosAddress: { - [CheqdNetwork.Testnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Testnet) - ?.address, - [CheqdNetwork.Mainnet]: paymentAccount.find((acc) => acc.namespace === CheqdNetwork.Mainnet) - ?.address, + [CheqdNetwork.Testnet]: testnetAddress ?? null, + [CheqdNetwork.Mainnet]: mainnetAddress ?? null, }, } satisfies AdminOrganisationGetResponseBody); } catch (error) { diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index d2a2a074..7455ac43 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -80,8 +80,8 @@ export class AccountController { name: response.locals.customer.name, }, paymentAccount: { - mainnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address ?? '', - testnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address ?? '', + mainnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address ?? null, + testnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address ?? null, }, }; diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 534b4899..4d6394c6 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -514,10 +514,12 @@ "properties": { "testnet": { "type": "string", - "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" + "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u", + "nullable": true }, "mainnet": { "type": "string", + "nullable": true, "example": "cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u" } } diff --git a/src/types/admin.ts b/src/types/admin.ts index 3b5cf3a0..a3266fd0 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -161,8 +161,8 @@ export type AdminOrganisationResponseBody = { email?: string; description?: string; cosmosAddress?: { - [CheqdNetwork.Testnet]?: string; - [CheqdNetwork.Mainnet]?: string; + [CheqdNetwork.Testnet]: string | null; + [CheqdNetwork.Mainnet]: string | null; }; }; diff --git a/src/types/customer.ts b/src/types/customer.ts index bc6211c6..fe21fcac 100644 --- a/src/types/customer.ts +++ b/src/types/customer.ts @@ -10,8 +10,8 @@ export type QueryCustomerResponseBody = { name: string; }; paymentAccount: { - [CheqdNetwork.Testnet]: string; - [CheqdNetwork.Mainnet]: string; + [CheqdNetwork.Testnet]: string | null; + [CheqdNetwork.Mainnet]: string | null; }; }; diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 040e746f..e78272fb 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -346,8 +346,10 @@ * testnet: * type: string * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u + * nullable: true * mainnet: * type: string + * nullable: true * example: cheqd1hwzvac94udsk8x4mf6htt544lev4jltkwgxp7u * NotFoundError: * description: The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible. From 8ca0de735b962f651810ea998672a99899288487 Mon Sep 17 00:00:00 2001 From: jay-dee7 Date: Tue, 6 Aug 2024 16:29:11 +0530 Subject: [PATCH 9/9] fix: Fallback to empty string on missing addresses in get account Signed-off-by: jay-dee7 --- src/controllers/api/account.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 7455ac43..d2a2a074 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -80,8 +80,8 @@ export class AccountController { name: response.locals.customer.name, }, paymentAccount: { - mainnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address ?? null, - testnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address ?? null, + mainnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Mainnet)?.address ?? '', + testnet: paymentAccounts.find((acc) => acc.namespace === CheqdNetwork.Testnet)?.address ?? '', }, };