Skip to content

Commit

Permalink
fix: Fix customer creation workflow [DEV-4287] (#574)
Browse files Browse the repository at this point in the history
* fix: Fix customer creation workflow

* fix swagger

* Generate migrations

* Add error handler for webhooks

* fix subscription create workflow

* Remove subscription-submitter

* Improve error handling

* fix: Account create validator

* Remove duplicate stripe initialization

* fix issues

---------

Co-authored-by: Ankur Banerjee <[email protected]>
  • Loading branch information
DaevMithran and ankurdotb committed Sep 6, 2024
1 parent c85f31a commit 6e1ca72
Show file tree
Hide file tree
Showing 13 changed files with 466 additions and 451 deletions.
413 changes: 349 additions & 64 deletions src/controllers/admin/webhook.ts

Large diffs are not rendered by default.

46 changes: 25 additions & 21 deletions src/controllers/api/account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import type { Request, Response } from 'express';
import type { CustomerEntity } from '../../database/entities/customer.entity.js';
import type { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js';
import type {
QueryCustomerResponseBody,
QueryIdTokenResponseBody,
UnsuccessfulQueryCustomerResponseBody,
UnsuccessfulQueryIdTokenResponseBody,
} from '../../types/customer.js';
import type { UnsuccessfulResponseBody } from '../../types/shared.js';
import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../../services/track/submitter.js';
import type Stripe from 'stripe';
import type { SafeAPIResponse } from '../../types/common.js';
import type { RoleEntity } from '../../database/entities/role.entity.js';
import type { SupportedPlanTypes } from '../../types/admin.js';

import { CheqdNetwork, checkBalance } from '@cheqd/sdk';
import { TESTNET_MINIMUM_BALANCE, DEFAULT_DENOM_EXPONENT, OperationNameEnum } from '../../types/constants.js';
import { CustomerService } from '../../services/api/customer.js';
Expand All @@ -8,39 +23,28 @@ import { StatusCodes } from 'http-status-codes';
import { LogToWebHook } from '../../middleware/hook.js';
import { UserService } from '../../services/api/user.js';
import { PaymentAccountService } from '../../services/api/payment-account.js';
import type { CustomerEntity } from '../../database/entities/customer.entity.js';
import type { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js';
import { IdentityServiceStrategySetup } from '../../services/identity/index.js';
import type {
QueryCustomerResponseBody,
QueryIdTokenResponseBody,
UnsuccessfulQueryCustomerResponseBody,
UnsuccessfulQueryIdTokenResponseBody,
} from '../../types/customer.js';
import type { UnsuccessfulResponseBody } from '../../types/shared.js';
import { check } from 'express-validator';
import { EventTracker, eventTracker } from '../../services/track/tracker.js';
import type { ISubmitOperation, ISubmitStripeCustomerCreateData } from '../../services/track/submitter.js';
import * as dotenv from 'dotenv';
import { validate } from '../validator/decorator.js';
import { SupportedKeyTypes } from '@veramo/utils';
import { SubscriptionService } from '../../services/admin/subscription.js';
import Stripe from 'stripe';
import { RoleService } from '../../services/api/role.js';
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';
dotenv.config();

export class AccountController {
public static createValidator = [
check('username')
check('primaryEmail')
.exists()
.withMessage('username is required')
.isString()
.withMessage('username should be a unique valid string'),
.withMessage('primaryEmail is required')
.bail()
.isEmail()
.withMessage('Invalid email id')
.bail(),
check('name').optional().isString().withMessage('name should be a valid string'),
];
/**
* @openapi
Expand Down Expand Up @@ -255,7 +259,7 @@ export class AccountController {

//4. Check if there is customer associated with such user
if (!userEntity.customer) {
customerEntity = (await CustomerService.instance.create(logToUserEmail)) as CustomerEntity;
customerEntity = (await CustomerService.instance.create(logToName, logToUserEmail)) as CustomerEntity;
if (!customerEntity) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'User exists in database: Customer was not created',
Expand Down Expand Up @@ -480,14 +484,14 @@ export class AccountController {
// 4. Check the token balance for Testnet account

// 1. Get logTo UserId from request body
const { username } = request.body;
const { name, primaryEmail } = request.body;

try {
// 2. Check if the customer exists
const customerEntity = response.locals.customer
? (response.locals.customer as CustomerEntity)
: // 2.1 Create customer
((await CustomerService.instance.create(username)) as CustomerEntity);
((await CustomerService.instance.create(name || primaryEmail, primaryEmail)) as CustomerEntity);

if (!customerEntity) {
return response.status(StatusCodes.BAD_REQUEST).json({
Expand Down
9 changes: 5 additions & 4 deletions src/database/entities/customer.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ export class CustomerEntity {

@Column({
type: 'text',
nullable: true,
nullable: false,
unique: true,
})
email?: string;
email!: string;

@Column({
type: 'text',
Expand Down Expand Up @@ -54,7 +55,7 @@ export class CustomerEntity {
this.updatedAt = new Date();
}

constructor(customerId: string, name: string, email?: string, description?: string, paymentProviderId?: string) {
constructor(customerId: string, name: string, email: string, description?: string, paymentProviderId?: string) {
this.customerId = customerId;
this.name = name;
this.email = email;
Expand All @@ -67,7 +68,7 @@ export class CustomerEntity {
public isEqual(customer: CustomerEntity): boolean {
return (
this.customerId === customer.customerId &&
this.name === customer.name &&
this.email === customer.email &&
this.createdAt.toISOString() === customer.createdAt.toISOString() &&
((!this.updatedAt && !customer.updatedAt) ||
this.updatedAt.toISOString() === customer.updatedAt.toISOString())
Expand Down
45 changes: 45 additions & 0 deletions src/database/migrations/AlterCustomerTableUniqueEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MigrationInterface, QueryRunner, TableColumn, TableUnique } from 'typeorm';

export class AlterCustomerTableUpdateEmail1695740346006 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const tableName = 'customer';

// Make the email column NOT NULL
await queryRunner.changeColumn(
tableName,
'email',
new TableColumn({
name: 'email',
type: 'text',
isNullable: false, // Set the column as NOT NULL
})
);

// Add a unique constraint to the email column
await queryRunner.createUniqueConstraint(
tableName,
new TableUnique({
name: 'UQ_customer_email',
columnNames: ['email'],
})
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
const tableName = 'customer';

// Remove the unique constraint
await queryRunner.dropUniqueConstraint(tableName, 'UQ_customer_email');

// Revert the email column back to nullable
await queryRunner.changeColumn(
tableName,
'email',
new TableColumn({
name: 'email',
type: 'text',
isNullable: true, // Revert to nullable
})
);
}
}
2 changes: 1 addition & 1 deletion src/database/migrations/MigrateData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class MigrateData1695740345977 implements MigrationInterface {
for (const oldCustomer of await queryRunner.query(`SELECT * FROM customers`)) {
// 1. Create customer row:
console.info(`Creating CustomerEntity with id ${oldCustomer.customerId}`);
const customerEntity = new CustomerEntity(uuidv4(), oldCustomer.address);
const customerEntity = new CustomerEntity(uuidv4(), oldCustomer.name, oldCustomer.email);
customerEntity.createdAt = new Date();

console.info(`Creating customer with address ${oldCustomer.address}`);
Expand Down
3 changes: 3 additions & 0 deletions src/database/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { SubscriptionEntity } from '../entities/subscription.entity.js';
import { CreateSubscritpionTable1695740346003 } from '../migrations/CreateSubscriptionTable.js';
import { AlterAPIKeyTable1695740346004 } from '../migrations/AlterAPIKeyTable.js';
import { AlterCustomerTableAddEmail1695740346005 } from '../migrations/AlterCustomerTableAddEmail.js';
import { AlterCustomerTableUpdateEmail1695740346006 } from '../migrations/AlterCustomerTableUniqueEmail.js';
dotenv.config();

const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env;
Expand Down Expand Up @@ -115,6 +116,8 @@ export class Postgres implements AbstractDatabase {
AlterAPIKeyTable1695740346004,
// Add email and description fields
AlterCustomerTableAddEmail1695740346005,
// Add unique constraint to email field
AlterCustomerTableUpdateEmail1695740346006,
],
entities: [
...Entities,
Expand Down
30 changes: 10 additions & 20 deletions src/services/admin/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@ import { SubscriptionService } from './subscription.js';
import type { CustomerEntity } from '../../database/entities/customer.entity.js';
import { EventTracker, eventTracker } from '../track/tracker.js';
import type { SubscriptionEntity } from '../../database/entities/subscription.entity.js';
import { buildSubmitOperation } from '../track/helpers.js';
import { OperationNameEnum } from '../../types/constants.js';
import { SubscriptionSubmitter } from '../track/admin/subscription-submitter.js';
import type { NextFunction } from 'express';
import { WebhookController } from '../../controllers/admin/webhook.js';

dotenv.config();

export class StripeService {
submitter: SubscriptionSubmitter;
private isFullySynced = false;
private stripe: Stripe;

constructor() {
this.submitter = new SubscriptionSubmitter(eventTracker.getEmitter());
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
}

async syncAll(next: NextFunction): Promise<void> {
Expand All @@ -28,9 +26,8 @@ export class StripeService {
}

async syncFull(): Promise<void> {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Sync all subscriptions
for await (const subscription of stripe.subscriptions.list({
for await (const subscription of this.stripe.subscriptions.list({
status: 'all',
})) {
const current = await SubscriptionService.instance.subscriptionRepository.findOne({
Expand All @@ -53,8 +50,7 @@ export class StripeService {

// Sync all the subscriptions for current customer
async syncCustomer(customer: CustomerEntity): Promise<void> {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
for await (const subscription of stripe.subscriptions.list({
for await (const subscription of this.stripe.subscriptions.list({
customer: customer.paymentProviderId,
status: 'all',
})) {
Expand All @@ -70,8 +66,6 @@ export class StripeService {
}

async syncOne(customer: CustomerEntity): Promise<void> {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

const local = await SubscriptionService.instance.findCurrent(customer);
if (!local) {
await eventTracker.notify({
Expand All @@ -81,11 +75,11 @@ export class StripeService {
),
severity: 'debug',
});
const activeSubs = await stripe.subscriptions.list({
const activeSubs = await this.stripe.subscriptions.list({
customer: customer.paymentProviderId,
status: 'active',
});
const trialSubs = await stripe.subscriptions.list({
const trialSubs = await this.stripe.subscriptions.list({
customer: customer.paymentProviderId,
status: 'trialing',
});
Expand All @@ -106,7 +100,7 @@ export class StripeService {
return;
}
const subscriptionId = local.subscriptionId;
const remote = await stripe.subscriptions.retrieve(subscriptionId);
const remote = await this.stripe.subscriptions.retrieve(subscriptionId);
if (!remote) {
await eventTracker.notify({
message: EventTracker.compileBasicNotification(
Expand All @@ -128,9 +122,7 @@ export class StripeService {
}

async createSubscription(subscription: Stripe.Subscription, customer?: CustomerEntity): Promise<void> {
await this.submitter.submitSubscriptionCreate(
buildSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_CREATE, { customer: customer })
);
await WebhookController.instance.handleSubscriptionCreate(this.stripe, subscription, customer);
}

async updateSubscription(subscription: Stripe.Subscription, current: SubscriptionEntity): Promise<void> {
Expand All @@ -145,9 +137,7 @@ export class StripeService {
});
return;
}
await this.submitter.submitSubscriptionUpdate(
buildSubmitOperation(subscription, OperationNameEnum.SUBSCRIPTION_UPDATE)
);
await WebhookController.instance.handleSubscriptionUpdate(this.stripe, subscription);
}
}

Expand Down
9 changes: 3 additions & 6 deletions src/services/api/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export class CustomerService {
this.customerRepository = Connection.instance.dbConnection.getRepository(CustomerEntity);
}

public async create(name: string, email?: string, description?: string, paymentProviderId?: string) {
public async create(name: string, email: string, description?: string, paymentProviderId?: string) {
// The sequence for creating a customer is supposed to be:
// 1. Create a new customer entity in the database;
// 2. Create new cosmos keypair
// 3. Get the cosmos address from the keypair
// 4. Create a new payment account entity in the database

if (await this.isExist({ name: name })) {
if (await this.isExist({ email: email })) {
throw new Error(`Cannot create a new customer since the customer with same name ${name} already exists`);
}
const customerEntity = new CustomerEntity(uuidv4(), name, email, description, paymentProviderId);
Expand All @@ -41,10 +41,7 @@ export class CustomerService {
customerEntity
);
await PaymentAccountService.instance.create(CheqdNetwork.Testnet, true, customerEntity, key);
return {
customerId: customerEntity.customerId,
name: customerEntity.name,
};
return customerEntity;
}

public async update(customer: UpdateCustomerEntity) {
Expand Down
Loading

0 comments on commit 6e1ca72

Please sign in to comment.