Merge pull request #972 from automatisch/payment-draft

feat: Initial payment implementation
This commit is contained in:
Ali BARIN
2023-03-05 18:14:03 +01:00
committed by GitHub
10 changed files with 268 additions and 4 deletions

View File

@@ -58,6 +58,7 @@
"oauth-1.0a": "^2.2.6",
"objection": "^3.0.0",
"pg": "^8.7.1",
"stripe": "^11.13.0",
"winston": "^3.7.1"
},
"contributors": [

View File

@@ -39,6 +39,9 @@ type AppConfig = {
smtpPassword: string;
fromEmail: string;
isCloud: boolean;
stripeSecretKey: string;
stripeStarterPriceKey: string;
stripeGrowthPriceKey: string;
licenseKey: string;
};
@@ -106,6 +109,9 @@ const appConfig: AppConfig = {
smtpPassword: process.env.SMTP_PASSWORD,
fromEmail: process.env.FROM_EMAIL,
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY,
stripeGrowthPriceKey: process.env.STRIPE_GROWTH_PRICE_KEY,
licenseKey: process.env.LICENSE_KEY,
};

View File

@@ -0,0 +1,24 @@
import { Knex } from 'knex';
import appConfig from '../../config/app';
export async function up(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.createTable('payment_plans', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('name').notNullable();
table.integer('task_count').notNullable();
table.uuid('user_id').references('id').inTable('users');
table.string('stripe_customer_id');
table.string('stripe_subscription_id');
table.timestamp('current_period_started_at').nullable();
table.timestamp('current_period_ends_at').nullable();
table.timestamp('deleted_at').nullable();
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.dropTable('payment_plans');
}

View File

@@ -0,0 +1,20 @@
import { Knex } from 'knex';
import appConfig from '../../config/app';
export async function up(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.createTable('usage_data', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('user_id').references('id').inTable('users');
table.string('consumed_task_count').notNullable();
table.timestamp('next_reset_at').nullable();
table.timestamp('deleted_at').nullable();
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.dropTable('usage_data');
}

View File

@@ -1,15 +1,17 @@
import User from '../../models/user';
import Billing from '../../helpers/billing/index.ee';
import appConfig from '../../config/app';
type Params = {
input: {
fullName: string;
email: string;
password: string;
fullName: string;
};
};
const createUser = async (_parent: unknown, params: Params) => {
const { email, password, fullName } = params.input;
const { fullName, email, password } = params.input;
const existingUser = await User.query().findOne({ email });
@@ -18,12 +20,16 @@ const createUser = async (_parent: unknown, params: Params) => {
}
const user = await User.query().insert({
fullName,
email,
password,
fullName,
role: 'user',
});
if (appConfig.isCloud) {
Billing.createSubscription(user);
}
return user;
};

View File

@@ -0,0 +1,96 @@
import Stripe from 'stripe';
import User from '../../models/user';
import PaymentPlan from '../../models/payment-plan.ee';
import UsageData from '../../models/usage-data.ee';
import appConfig from '../../config/app';
const plans = [
{
price: appConfig.stripeStarterPriceKey,
name: 'Starter',
taskCount: 1000,
default: true,
},
{
price: appConfig.stripeGrowthPriceKey,
name: 'Growth',
taskCount: 10000,
default: false,
},
];
const stripe = new Stripe(appConfig.stripeSecretKey, {
apiVersion: '2022-11-15',
});
const createStripeCustomer = async (user: User) => {
const params: Stripe.CustomerCreateParams = {
email: user.email,
name: user.fullName,
description: `User ID: ${user.id}`,
};
return await stripe.customers.create(params);
};
const defaultPlan = plans.find((plan) => plan.default);
const createStripeSubscription = async (
user: User,
stripeCustomer: Stripe.Customer
) => {
const params: Stripe.SubscriptionCreateParams = {
customer: stripeCustomer.id,
items: [{ price: defaultPlan.price }],
};
return await stripe.subscriptions.create(params);
};
const createSubscription = async (user: User) => {
const stripeCustomer = await createStripeCustomer(user);
const stripeSubscription = await createStripeSubscription(
user,
stripeCustomer
);
await PaymentPlan.query().insert({
name: defaultPlan.name,
taskCount: defaultPlan.taskCount,
userId: user.id,
stripeCustomerId: stripeCustomer.id,
stripeSubscriptionId: stripeSubscription.id,
currentPeriodStartedAt: new Date(
stripeSubscription.current_period_start * 1000
).toISOString(),
currentPeriodEndsAt: new Date(
stripeSubscription.current_period_end * 1000
).toISOString(),
});
await UsageData.query().insert({
userId: user.id,
consumedTaskCount: 0,
nextResetAt: new Date(
stripeSubscription.current_period_end * 1000
).toISOString(),
});
};
const createPaymentPortalUrl = async (user: User) => {
const paymentPlan = await user.$relatedQuery('paymentPlan');
const userSession = await stripe.billingPortal.sessions.create({
customer: paymentPlan.stripeCustomerId,
return_url: 'https://cloud.automatisch.io',
});
return userSession.url;
};
const billing = {
createSubscription,
createPaymentPortalUrl,
};
export default billing;

View File

@@ -0,0 +1,52 @@
import Base from './base';
import User from './user';
class PaymentPlan extends Base {
id!: string;
name!: string;
taskCount: number;
userId!: string;
stripeCustomerId!: string;
stripeSubscriptionId!: string;
currentPeriodStartedAt!: string;
currentPeriodEndsAt!: string;
static tableName = 'payment_plans';
static jsonSchema = {
type: 'object',
required: [
'name',
'taskCount',
'userId',
'stripeCustomerId',
'stripeSubscriptionId',
'currentPeriodStartedAt',
'currentPeriodEndsAt',
],
properties: {
id: { type: 'string', format: 'uuid' },
name: { type: 'string' },
taskCount: { type: 'integer' },
userId: { type: 'string', format: 'uuid' },
stripeCustomerId: { type: 'string' },
stripeSubscriptionId: { type: 'string' },
currentPeriodStartedAt: { type: 'string' },
currentPeriodEndsAt: { type: 'string' },
},
};
static relationMappings = () => ({
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'payment_plans.user_id',
to: 'users.id',
},
},
});
}
export default PaymentPlan;

View File

@@ -0,0 +1,36 @@
import Base from './base';
import User from './user';
class UsageData extends Base {
id!: string;
userId!: string;
consumedTaskCount!: number;
nextResetAt!: string;
static tableName = 'usage_data';
static jsonSchema = {
type: 'object',
required: ['userId', 'consumedTaskCount', 'nextResetAt'],
properties: {
id: { type: 'string', format: 'uuid' },
userId: { type: 'string', format: 'uuid' },
consumedTaskCount: { type: 'integer' },
nextResetAt: { type: 'string' },
},
};
static relationMappings = () => ({
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'payment_plans.user_id',
to: 'users.id',
},
},
});
}
export default UsageData;

View File

@@ -6,6 +6,7 @@ import Step from './step';
import Execution from './execution';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import PaymentPlan from './payment-plan.ee';
class User extends Base {
id!: string;
@@ -19,6 +20,7 @@ class User extends Base {
flows?: Flow[];
steps?: Step[];
executions?: Execution[];
paymentPlan?: PaymentPlan;
static tableName = 'users';
@@ -76,6 +78,14 @@ class User extends Base {
to: 'executions.flow_id',
},
},
paymentPlan: {
relation: Base.HasOneRelation,
modelClass: PaymentPlan,
join: {
from: 'payment_plans.user_id',
to: 'users.id',
},
},
});
login(password: string) {

View File

@@ -4009,6 +4009,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.10.tgz#616f16e9d3a2a3d618136b1be244315d95bd7cab"
integrity sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==
"@types/node@>=8.1.0":
version "18.14.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.5.tgz#4a13a6445862159303fc38586598a9396fc408b3"
integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw==
"@types/node@^12.0.0":
version "12.20.42"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.42.tgz#2f021733232c2130c26f9eabbdd3bfd881774733"
@@ -14379,7 +14384,7 @@ qqjs@^0.3.11:
tmp "^0.1.0"
write-json-file "^4.1.1"
qs@6.11.0, qs@^6.9.4:
qs@6.11.0, qs@^6.11.0, qs@^6.9.4:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
@@ -16190,6 +16195,14 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
stripe@^11.13.0:
version "11.13.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-11.13.0.tgz#c930f618b3a111a7618cd582cd8c7a7e54aaaf9e"
integrity sha512-Jx0nDbdvRsTtDSX5OFQ+4rLmYIftoiOE9HAXWIgyhAz1QjRFI3UIiJ/kCyhkdJBoHu019O5Ya6EmQ5Zf635XDw==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.11.0"
strnum@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"