Merge pull request #972 from automatisch/payment-draft
feat: Initial payment implementation
This commit is contained in:
@@ -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": [
|
||||
|
@@ -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,
|
||||
};
|
||||
|
||||
|
@@ -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');
|
||||
}
|
@@ -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');
|
||||
}
|
@@ -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;
|
||||
};
|
||||
|
||||
|
96
packages/backend/src/helpers/billing/index.ee.ts
Normal file
96
packages/backend/src/helpers/billing/index.ee.ts
Normal 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;
|
52
packages/backend/src/models/payment-plan.ee.ts
Normal file
52
packages/backend/src/models/payment-plan.ee.ts
Normal 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;
|
36
packages/backend/src/models/usage-data.ee.ts
Normal file
36
packages/backend/src/models/usage-data.ee.ts
Normal 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;
|
@@ -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) {
|
||||
|
15
yarn.lock
15
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user