Add billing tables (#8772)
Beforehand, the name of the branch is not representative of the work that has been done in this PR **TLDR:** Solves https://github.com/twentyhq/private-issues/issues/192 Add 3 tables BillingCustomer, BillingProduct and BillingPrice and BillingMeter to core, inspired by the Stripe implementation. Separates migration, between common and billing on order to not populate the db of the self-hosting instances with unused tables. **In order to test:** Run the command: npx nx typeorm -- migration:run -d src/database/typeorm/core/core.datasource.ts **Considerations:** I only put the information we should use right now in the Billing module, for instance columns like meter or agreggation formula where omitted in the creation of the tables. These columns and other ones who fall on the same spectrum will be added as we need them. If you want to add more information to the table, I'll leave some utility links down bellow: - BillingPrices: https://docs.stripe.com/api/prices/object - BillingCustomer: https://docs.stripe.com/api/customers/object - BillingProduct: https://docs.stripe.com/api/products/object **Next Steps** Use the Stripe Webhook in order to update the tables accordingly --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
committed by
GitHub
parent
c993f2de0b
commit
11d244194f
@ -18,9 +18,15 @@ export const typeORMCoreModuleOptions: TypeOrmModuleOptions = {
|
||||
migrationsRun: false,
|
||||
migrationsTableName: '_typeorm_migrations',
|
||||
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
||||
migrations: [
|
||||
`${isJest ? '' : 'dist/'}src/database/typeorm/core/migrations/*{.ts,.js}`,
|
||||
],
|
||||
migrations:
|
||||
process.env.IS_BILLING_ENABLED === 'true'
|
||||
? [
|
||||
`${isJest ? '' : 'dist/'}src/database/typeorm/core/migrations/common/*{.ts,.js}`,
|
||||
`${isJest ? '' : 'dist/'}src/database/typeorm/core/migrations/billing/*{.ts,.js}`,
|
||||
]
|
||||
: [
|
||||
`${isJest ? '' : 'dist/'}src/database/typeorm/core/migrations/common/*{.ts,.js}`,
|
||||
],
|
||||
ssl:
|
||||
process.env.PG_SSL_ALLOW_SELF_SIGNED === 'true'
|
||||
? {
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIntervalToBillingSubscription1710926613773
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddIntervalToBillingSubscription1710926613773';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "interval" character varying`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "interval"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,9 @@ export class UpdateBillingCoreTables1709233666080
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD CONSTRAINT "IndexOnBillingSubscriptionIdAndStripeProductIdUnique" UNIQUE ("billingSubscriptionId", "stripeProductId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ALTER COLUMN "deletedAt" TYPE TIMESTAMP WITH TIME ZONE USING "deletedAt" AT TIME ZONE 'UTC'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
@ -27,5 +30,8 @@ export class UpdateBillingCoreTables1709233666080
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "stripeSubscriptionItemId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ALTER COLUMN "deletedAt" TYPE TIMESTAMP`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -18,9 +18,27 @@ export class UpdateBillingSubscription1709914564361
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD CONSTRAINT "FK_4abfb70314c18da69e1bee1954d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "deletedAt" TYPE TIMESTAMP WITH TIME ZONE USING "deletedAt" AT TIME ZONE 'UTC'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingSubscription_status_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE "core"."billingSubscription_status_enum" USING "status"::"core"."billingSubscription_status_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_4abfb70314c18da69e1bee1954d"`,
|
||||
);
|
||||
@ -33,5 +51,17 @@ export class UpdateBillingSubscription1709914564361
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD CONSTRAINT "FK_4abfb70314c18da69e1bee1954d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."billingSubscription_status_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "deletedAt" TYPE TIMESTAMP`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddIntervalToBillingSubscription1710926613773
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddIntervalToBillingSubscription1710926613773';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "interval" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingSubscription_interval_enum" AS ENUM('day', 'month', 'week', 'year')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE "core"."billingSubscription_interval_enum" USING "interval"::"core"."billingSubscription_interval_enum"`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "interval"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."billingSubscription_interval_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddNewBillingStripeTables1733397937967
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddNewBillingStripeTables1733397937967';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."billingCustomer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP WITH TIME ZONE, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "workspaceId" uuid NOT NULL, "stripeCustomerId" character varying NOT NULL, CONSTRAINT "UQ_b35a0ef2e2f0d40101dd7f161b9" UNIQUE ("stripeCustomerId"), CONSTRAINT "IndexOnWorkspaceIdAndStripeCustomerIdUnique" UNIQUE ("workspaceId", "stripeCustomerId"), CONSTRAINT "PK_5fffcd69bf722c297a3d5c3f3bc" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingMeter_status_enum" AS ENUM('ACTIVE', 'INACTIVE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingMeter_eventtimewindow_enum" AS ENUM('DAY', 'HOUR')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."billingMeter" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP WITH TIME ZONE, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "stripeMeterId" character varying NOT NULL, "displayName" character varying NOT NULL, "eventName" character varying NOT NULL, "status" "core"."billingMeter_status_enum" NOT NULL, "customerMapping" jsonb NOT NULL, "eventTimeWindow" "core"."billingMeter_eventtimewindow_enum", "valueSettings" jsonb NOT NULL, CONSTRAINT "UQ_340c08c4e5dd33cf963cbb133ae" UNIQUE ("stripeMeterId"), CONSTRAINT "PK_0bba5f7d2e3713332a0138ea1b3" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."billingProduct" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP WITH TIME ZONE, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "active" boolean NOT NULL, "description" text, "name" character varying NOT NULL, "taxCode" text, "images" jsonb NOT NULL DEFAULT '[]', "marketingFeatures" jsonb NOT NULL DEFAULT '[]', "stripeProductId" character varying NOT NULL, "defaultStripePriceId" text, "metadata" jsonb NOT NULL DEFAULT '{}', "unitLabel" text, "url" text, CONSTRAINT "UQ_1ba1ba118792aa9eec92f132e82" UNIQUE ("stripeProductId"), CONSTRAINT "PK_8bb3c7be66db8e05476808b0ca7" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingPrice_taxbehavior_enum" AS ENUM('EXCLUSIVE', 'INCLUSIVE', 'UNSPECIFIED')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingPrice_type_enum" AS ENUM('ONE_TIME', 'RECURRING')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingPrice_billingscheme_enum" AS ENUM('PER_UNIT', 'TIERED')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingPrice_tiersmode_enum" AS ENUM('GRADUATED', 'VOLUME')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingPrice_usagetype_enum" AS ENUM('METERED', 'LICENSED')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingPrice_interval_enum" AS ENUM('day', 'month', 'week', 'year')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."billingPrice" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deletedAt" TIMESTAMP WITH TIME ZONE, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "stripePriceId" character varying NOT NULL, "active" boolean NOT NULL, "stripeProductId" character varying NOT NULL, "currency" character varying NOT NULL, "nickname" text, "taxBehavior" "core"."billingPrice_taxbehavior_enum" NOT NULL, "type" "core"."billingPrice_type_enum" NOT NULL, "billingScheme" "core"."billingPrice_billingscheme_enum" NOT NULL, "currencyOptions" jsonb, "tiers" jsonb, "recurring" jsonb, "transformQuantity" jsonb, "tiersMode" "core"."billingPrice_tiersmode_enum", "unitAmountDecimal" text, "unitAmount" numeric, "stripeMeterId" character varying, "usageType" "core"."billingPrice_usagetype_enum" NOT NULL, "interval" "core"."billingPrice_interval_enum", CONSTRAINT "UQ_f66d20a329f5f4b9d12afeae7d0" UNIQUE ("stripePriceId"), CONSTRAINT "PK_13927aef8d4e68e176a61c33d89" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "stripeSubscriptionId" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "metadata" jsonb NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "billingThresholds" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "cancelAtPeriodEnd" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "currency" character varying NOT NULL DEFAULT 'USD'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "currentPeriodEnd" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "currentPeriodStart" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "metadata" jsonb NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "cancelAt" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "canceledAt" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "automaticTax" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "cancellationDetails" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingSubscription_collectionmethod_enum" AS ENUM('CHARGE_AUTOMATICALLY', 'SEND_INVOICE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "collectionMethod" "core"."billingSubscription_collectionmethod_enum" NOT NULL DEFAULT 'CHARGE_AUTOMATICALLY'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "endedAt" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "trialStart" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ADD "trialEnd" TIMESTAMP WITH TIME ZONE`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingCustomer" ADD CONSTRAINT "FK_53c2ef50e9611082f83d760897d" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingPrice" ADD CONSTRAINT "FK_4d57ee4dbfc8b4075eb24026fca" FOREIGN KEY ("stripeProductId") REFERENCES "core"."billingProduct"("stripeProductId") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingPrice" ADD CONSTRAINT "FK_c8b4375b7bf8724ba54065372e1" FOREIGN KEY ("stripeMeterId") REFERENCES "core"."billingMeter"("stripeMeterId") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingPrice" DROP CONSTRAINT "FK_c8b4375b7bf8724ba54065372e1"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingPrice" DROP CONSTRAINT "FK_4d57ee4dbfc8b4075eb24026fca"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingCustomer" DROP CONSTRAINT "FK_53c2ef50e9611082f83d760897d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "trialEnd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "trialStart"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "endedAt"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "collectionMethod"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."billingSubscription_collectionmethod_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "cancellationDetails"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "automaticTax"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "canceledAt"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "cancelAt"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "metadata"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "currentPeriodStart"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "currentPeriodEnd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "currency"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" DROP COLUMN "cancelAtPeriodEnd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "billingThresholds"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "metadata"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "stripeSubscriptionId"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."billingPrice"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."billingPrice_interval_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."billingPrice_usagetype_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."billingPrice_tiersmode_enum"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."billingPrice_billingscheme_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE "core"."billingPrice_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."billingPrice_taxbehavior_enum"`);
|
||||
await queryRunner.query(`DROP TABLE "core"."billingProduct"`);
|
||||
await queryRunner.query(`DROP TABLE "core"."billingMeter"`);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."billingMeter_eventtimewindow_enum"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE "core"."billingMeter_status_enum"`);
|
||||
await queryRunner.query(`DROP TABLE "core"."billingCustomer"`);
|
||||
}
|
||||
}
|
||||
@ -10,12 +10,7 @@ export class UseTimestampWithTZ1711633823798 implements MigrationInterface {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."featureFlag" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE USING "updatedAt" AT TIME ZONE 'UTC'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ALTER COLUMN "deletedAt" TYPE TIMESTAMP WITH TIME ZONE USING "deletedAt" AT TIME ZONE 'UTC'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "deletedAt" TYPE TIMESTAMP WITH TIME ZONE USING "deletedAt" AT TIME ZONE 'UTC'`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ALTER COLUMN "deletedAt" TYPE TIMESTAMP WITH TIME ZONE USING "deletedAt" AT TIME ZONE 'UTC'`,
|
||||
);
|
||||
@ -43,12 +38,7 @@ export class UseTimestampWithTZ1711633823798 implements MigrationInterface {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."featureFlag" ALTER COLUMN "updatedAt" TYPE TIMESTAMP`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscriptionItem" ALTER COLUMN "deletedAt" TYPE TIMESTAMP`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "deletedAt" TYPE TIMESTAMP`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ALTER COLUMN "deletedAt" TYPE TIMESTAMP`,
|
||||
);
|
||||
@ -9,12 +9,7 @@ export class UpdateInconsistentUserConstraint1715593226719
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."user" DROP CONSTRAINT "FK_2ec910029395fa7655621c88908"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE text`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" TYPE text`,
|
||||
);
|
||||
@ -30,12 +25,6 @@ export class UpdateInconsistentUserConstraint1715593226719
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."user" ADD CONSTRAINT "FK_2ec910029395fa7655621c88908" FOREIGN KEY ("defaultWorkspaceId") REFERENCES "core"."workspace"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
@ -6,21 +6,6 @@ export class UseEnumForSubscriptionStatusInterval1719327438923
|
||||
name = 'UseEnumForSubscriptionStatusInterval1719327438923';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingSubscription_status_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE "core"."billingSubscription_status_enum" USING "status"::"core"."billingSubscription_status_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."billingSubscription_interval_enum" AS ENUM('day', 'month', 'week', 'year')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE "core"."billingSubscription_interval_enum" USING "interval"::"core"."billingSubscription_interval_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."workspace_subscriptionstatus_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
|
||||
);
|
||||
@ -45,17 +30,5 @@ export class UseEnumForSubscriptionStatusInterval1719327438923
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."workspace_subscriptionstatus_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."billingSubscription_interval_enum"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE text`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP TYPE "core"."billingSubscription_status_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,11 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
@ -36,6 +40,10 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy {
|
||||
FeatureFlagEntity,
|
||||
BillingSubscription,
|
||||
BillingSubscriptionItem,
|
||||
BillingMeter,
|
||||
BillingCustomer,
|
||||
BillingProduct,
|
||||
BillingPrice,
|
||||
BillingEntitlement,
|
||||
PostgresCredentials,
|
||||
WorkspaceSSOIdentityProvider,
|
||||
|
||||
@ -3,7 +3,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BillingController } from 'src/engine/core-modules/billing/billing.controller';
|
||||
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
|
||||
@ -27,6 +31,10 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
|
||||
[
|
||||
BillingSubscription,
|
||||
BillingSubscriptionItem,
|
||||
BillingCustomer,
|
||||
BillingProduct,
|
||||
BillingPrice,
|
||||
BillingMeter,
|
||||
BillingEntitlement,
|
||||
Workspace,
|
||||
UserWorkspace,
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Entity({ name: 'billingCustomer', schema: 'core' })
|
||||
@ObjectType('billingCustomer')
|
||||
@Unique('IndexOnWorkspaceIdAndStripeCustomerIdUnique', [
|
||||
'workspaceId',
|
||||
'stripeCustomerId',
|
||||
])
|
||||
export class BillingCustomer {
|
||||
@IDField(() => UUIDScalarType)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.billingCustomers, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
workspace: Relation<Workspace>;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
workspaceId: string;
|
||||
|
||||
@Column({ nullable: false, unique: true })
|
||||
stripeCustomerId: string;
|
||||
|
||||
@OneToMany(
|
||||
() => BillingSubscription,
|
||||
(billingSubscription) => billingSubscription.billingCustomer,
|
||||
)
|
||||
billingSubscriptions: Relation<BillingSubscription[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => BillingEntitlement,
|
||||
(billingEntitlement) => billingEntitlement.billingCustomer,
|
||||
)
|
||||
billingEntitlements: Relation<BillingEntitlement[]>;
|
||||
}
|
||||
@ -14,6 +14,7 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@Entity({ name: 'billingEntitlement', schema: 'core' })
|
||||
@ -53,4 +54,17 @@ export class BillingEntitlement {
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
@ManyToOne(
|
||||
() => BillingCustomer,
|
||||
(billingCustomer) => billingCustomer.billingEntitlements,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
createForeignKeyConstraints: false, // TODO: remove this once the customer table is populated
|
||||
},
|
||||
)
|
||||
@JoinColumn({
|
||||
referencedColumnName: 'stripeCustomerId',
|
||||
name: 'stripeCustomerId',
|
||||
})
|
||||
billingCustomer: Relation<BillingCustomer>;
|
||||
}
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
|
||||
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
|
||||
|
||||
@Entity({ name: 'billingMeter', schema: 'core' })
|
||||
export class BillingMeter {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: false, unique: true })
|
||||
stripeMeterId: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
displayName: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
eventName: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingMeterStatus),
|
||||
})
|
||||
status: BillingMeterStatus;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb' })
|
||||
customerMapping: Stripe.Billing.Meter.CustomerMapping;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingMeterEventTimeWindow),
|
||||
})
|
||||
eventTimeWindow: BillingMeterEventTimeWindow | null;
|
||||
|
||||
@OneToMany(() => BillingPrice, (billingPrice) => billingPrice.billingMeter)
|
||||
billingPrices: Relation<BillingPrice[]>;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb' })
|
||||
valueSettings: Stripe.Billing.Meter.ValueSettings;
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
import { Field } from '@nestjs/graphql';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity';
|
||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||
import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum';
|
||||
import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum';
|
||||
import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum';
|
||||
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
@Entity({ name: 'billingPrice', schema: 'core' })
|
||||
export class BillingPrice {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: false, unique: true })
|
||||
stripePriceId: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
active: boolean;
|
||||
|
||||
@Column({ nullable: false })
|
||||
stripeProductId: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
currency: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
nickname: string | null;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingPriceTaxBehavior),
|
||||
})
|
||||
taxBehavior: BillingPriceTaxBehavior;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingPriceType),
|
||||
})
|
||||
type: BillingPriceType;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingPriceBillingScheme),
|
||||
})
|
||||
billingScheme: BillingPriceBillingScheme;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
currencyOptions: Stripe.Price.CurrencyOptions | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
tiers: Stripe.Price.Tier[] | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
recurring: Stripe.Price.Recurring | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
transformQuantity: Stripe.Price.TransformQuantity | null;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingPriceTiersMode),
|
||||
})
|
||||
tiersMode: BillingPriceTiersMode | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
unitAmountDecimal: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'numeric' })
|
||||
unitAmount: number | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
stripeMeterId: string | null;
|
||||
|
||||
@Field(() => BillingUsageType)
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingUsageType),
|
||||
nullable: false,
|
||||
})
|
||||
usageType: BillingUsageType;
|
||||
|
||||
@Field(() => SubscriptionInterval, { nullable: true })
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: Object.values(SubscriptionInterval),
|
||||
nullable: true,
|
||||
})
|
||||
interval: SubscriptionInterval | null;
|
||||
|
||||
@ManyToOne(
|
||||
() => BillingProduct,
|
||||
(billingProduct) => billingProduct.billingPrices,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
)
|
||||
@JoinColumn({
|
||||
referencedColumnName: 'stripeProductId',
|
||||
name: 'stripeProductId',
|
||||
})
|
||||
billingProduct: Relation<BillingProduct>;
|
||||
|
||||
@ManyToOne(() => BillingMeter, (billingMeter) => billingMeter.billingPrices, {
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn({
|
||||
referencedColumnName: 'stripeMeterId',
|
||||
name: 'stripeMeterId',
|
||||
})
|
||||
billingMeter: Relation<BillingMeter>;
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { BillingProductMetadata } from 'src/engine/core-modules/billing/types/billing-product-metadata.type';
|
||||
registerEnumType(BillingUsageType, { name: 'BillingUsageType' });
|
||||
@Entity({ name: 'billingProduct', schema: 'core' })
|
||||
export class BillingProduct {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: false })
|
||||
active: boolean;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
description: string | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
taxCode: string | null;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb', default: [] })
|
||||
images: string[];
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb', default: [] })
|
||||
marketingFeatures: Stripe.Product.MarketingFeature[];
|
||||
|
||||
@Column({ nullable: false, unique: true })
|
||||
stripeProductId: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
defaultStripePriceId: string | null;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb', default: {} })
|
||||
metadata: BillingProductMetadata;
|
||||
|
||||
@OneToMany(() => BillingPrice, (billingPrice) => billingPrice.billingProduct)
|
||||
billingPrices: Relation<BillingPrice[]>;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
unitLabel: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
url: string | null;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import Stripe from 'stripe';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@ -10,7 +11,6 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
|
||||
@Entity({ name: 'billingSubscriptionItem', schema: 'core' })
|
||||
@Unique('IndexOnBillingSubscriptionIdAndStripeProductIdUnique', [
|
||||
'billingSubscriptionId',
|
||||
@ -36,6 +36,15 @@ export class BillingSubscriptionItem {
|
||||
@Column({ nullable: false })
|
||||
billingSubscriptionId: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
stripeSubscriptionId: string;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb', default: {} })
|
||||
metadata: Stripe.Metadata;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
billingThresholds: Stripe.SubscriptionItem.BillingThresholds;
|
||||
|
||||
@ManyToOne(
|
||||
() => BillingSubscription,
|
||||
(billingSubscription) => billingSubscription.billingSubscriptionItems,
|
||||
@ -52,8 +61,8 @@ export class BillingSubscriptionItem {
|
||||
stripePriceId: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
stripeSubscriptionItemId: string;
|
||||
stripeSubscriptionItemId: string; //TODO: add unique
|
||||
|
||||
@Column({ nullable: false })
|
||||
quantity: number;
|
||||
quantity: number; //TODO: add nullable and modify stripe service
|
||||
}
|
||||
|
||||
@ -15,7 +15,9 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@ -75,4 +77,73 @@ export class BillingSubscription {
|
||||
(billingSubscriptionItem) => billingSubscriptionItem.billingSubscription,
|
||||
)
|
||||
billingSubscriptionItems: Relation<BillingSubscriptionItem[]>;
|
||||
|
||||
@ManyToOne(
|
||||
() => BillingCustomer,
|
||||
(billingCustomer) => billingCustomer.billingSubscriptions,
|
||||
{
|
||||
nullable: false,
|
||||
createForeignKeyConstraints: false,
|
||||
},
|
||||
)
|
||||
@JoinColumn({
|
||||
referencedColumnName: 'stripeCustomerId',
|
||||
name: 'stripeCustomerId',
|
||||
})
|
||||
billingCustomer: Relation<BillingCustomer>; //let's see if it works
|
||||
|
||||
@Column({ nullable: false, default: false })
|
||||
cancelAtPeriodEnd: boolean;
|
||||
|
||||
@Column({ nullable: false, default: 'USD' })
|
||||
currency: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'timestamptz',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
currentPeriodEnd: Date;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'timestamptz',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
currentPeriodStart: Date;
|
||||
|
||||
@Column({ nullable: false, type: 'jsonb', default: {} })
|
||||
metadata: Stripe.Metadata;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
cancelAt: Date | null;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: 'timestamptz',
|
||||
})
|
||||
canceledAt: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
automaticTax: Stripe.Subscription.AutomaticTax | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
cancellationDetails: Stripe.Subscription.CancellationDetails | null;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'enum',
|
||||
enum: Object.values(BillingSubscriptionCollectionMethod),
|
||||
default: BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY,
|
||||
})
|
||||
collectionMethod: BillingSubscriptionCollectionMethod;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
endedAt: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
trialStart: Date | null;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
trialEnd: Date | null;
|
||||
}
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingMeterEventTimeWindow {
|
||||
DAY = 'DAY',
|
||||
HOUR = 'HOUR',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingMeterStatus {
|
||||
ACTIVE = 'ACTIVE',
|
||||
INACTIVE = 'INACTIVE',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingPlanKey {
|
||||
BASE_PLAN = 'BASE_PLAN',
|
||||
PRO_PLAN = 'PRO_PLAN',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingPriceBillingScheme {
|
||||
PER_UNIT = 'PER_UNIT',
|
||||
TIERED = 'TIERED',
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export enum BillingPriceTaxBehavior {
|
||||
EXCLUSIVE = 'EXCLUSIVE',
|
||||
INCLUSIVE = 'INCLUSIVE',
|
||||
UNSPECIFIED = 'UNSPECIFIED',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingPriceTiersMode {
|
||||
GRADUATED = 'GRADUATED',
|
||||
VOLUME = 'VOLUME',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingPriceType {
|
||||
ONE_TIME = 'ONE_TIME',
|
||||
RECURRING = 'RECURRING',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingSubscriptionCollectionMethod {
|
||||
CHARGE_AUTOMATICALLY = 'CHARGE_AUTOMATICALLY',
|
||||
SEND_INVOICE = 'SEND_INVOICE',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum BillingUsageType {
|
||||
METERED = 'METERED',
|
||||
LICENSED = 'LICENSED',
|
||||
}
|
||||
@ -2,10 +2,11 @@ import { Logger, Scope } from '@nestjs/common';
|
||||
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
export type UpdateSubscriptionJobData = { workspaceId: string };
|
||||
|
||||
@Processor({
|
||||
@ -17,15 +18,18 @@ export class UpdateSubscriptionJob {
|
||||
|
||||
constructor(
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
@Process(UpdateSubscriptionJob.name)
|
||||
async handle(data: UpdateSubscriptionJobData): Promise<void> {
|
||||
const workspaceMembersCount = await this.userWorkspaceService.getUserCount(
|
||||
data.workspaceId,
|
||||
);
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMembersCount = await workspaceMemberRepository.count();
|
||||
|
||||
if (!workspaceMembersCount || workspaceMembersCount <= 0) {
|
||||
return;
|
||||
|
||||
@ -12,6 +12,7 @@ import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/bil
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import {
|
||||
Workspace,
|
||||
@ -54,6 +55,30 @@ export class BillingWebhookService {
|
||||
stripeSubscriptionId: data.object.id,
|
||||
status: data.object.status as SubscriptionStatus,
|
||||
interval: data.object.items.data[0].plan.interval,
|
||||
cancelAtPeriodEnd: data.object.cancel_at_period_end,
|
||||
currency: data.object.currency.toUpperCase(),
|
||||
currentPeriodEnd: new Date(data.object.current_period_end * 1000),
|
||||
currentPeriodStart: new Date(data.object.current_period_start * 1000),
|
||||
metadata: data.object.metadata,
|
||||
collectionMethod:
|
||||
data.object.collection_method.toUpperCase() as BillingSubscriptionCollectionMethod,
|
||||
automaticTax: data.object.automatic_tax ?? undefined,
|
||||
cancellationDetails: data.object.cancellation_details ?? undefined,
|
||||
endedAt: data.object.ended_at
|
||||
? new Date(data.object.ended_at * 1000)
|
||||
: undefined,
|
||||
trialStart: data.object.trial_start
|
||||
? new Date(data.object.trial_start * 1000)
|
||||
: undefined,
|
||||
trialEnd: data.object.trial_end
|
||||
? new Date(data.object.trial_end * 1000)
|
||||
: undefined,
|
||||
cancelAt: data.object.cancel_at
|
||||
? new Date(data.object.cancel_at * 1000)
|
||||
: undefined,
|
||||
canceledAt: data.object.canceled_at
|
||||
? new Date(data.object.canceled_at * 1000)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
conflictPaths: ['stripeSubscriptionId'],
|
||||
@ -70,10 +95,13 @@ export class BillingWebhookService {
|
||||
data.object.items.data.map((item) => {
|
||||
return {
|
||||
billingSubscriptionId: billingSubscription.id,
|
||||
stripeSubscriptionId: data.object.id,
|
||||
stripeProductId: item.price.product as string,
|
||||
stripePriceId: item.price.id,
|
||||
stripeSubscriptionItemId: item.id,
|
||||
quantity: item.quantity,
|
||||
metadata: item.metadata,
|
||||
billingThresholds: item.billing_thresholds ?? undefined,
|
||||
};
|
||||
}),
|
||||
{
|
||||
|
||||
@ -22,7 +22,10 @@ export class BillingService {
|
||||
return this.environmentService.get('IS_BILLING_ENABLED');
|
||||
}
|
||||
|
||||
async hasWorkspaceActiveSubscriptionOrFreeAccess(workspaceId: string) {
|
||||
async hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||
workspaceId: string,
|
||||
entitlementKey?: BillingEntitlementKey,
|
||||
) {
|
||||
const isBillingEnabled = this.isBillingEnabled();
|
||||
|
||||
if (!isBillingEnabled) {
|
||||
@ -39,6 +42,13 @@ export class BillingService {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entitlementKey) {
|
||||
return this.billingSubscriptionService.getWorkspaceEntitlementByKey(
|
||||
workspaceId,
|
||||
entitlementKey,
|
||||
);
|
||||
}
|
||||
|
||||
const currentBillingSubscription =
|
||||
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId },
|
||||
@ -53,20 +63,4 @@ export class BillingService {
|
||||
].includes(currentBillingSubscription.status)
|
||||
);
|
||||
}
|
||||
|
||||
async verifyWorkspaceEntitlement(
|
||||
workspaceId: string,
|
||||
entitlementKey: BillingEntitlementKey,
|
||||
) {
|
||||
const isBillingEnabled = this.isBillingEnabled();
|
||||
|
||||
if (!isBillingEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.billingSubscriptionService.getWorkspaceEntitlementByKey(
|
||||
workspaceId,
|
||||
entitlementKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
export type BillingProductMetadata = {
|
||||
planKey: BillingPlanKey;
|
||||
priceUsageBased: BillingUsageType;
|
||||
};
|
||||
@ -24,11 +24,11 @@ import { LLMTracingDriver } from 'src/engine/core-modules/llm-tracing/interfaces
|
||||
|
||||
import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
|
||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { AssertOrWarn } from 'src/engine/core-modules/environment/decorators/assert-or-warn.decorator';
|
||||
import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator';
|
||||
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
|
||||
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
|
||||
import { CastToStringArray } from 'src/engine/core-modules/environment/decorators/cast-to-string-array.decorator';
|
||||
import { AssertOrWarn } from 'src/engine/core-modules/environment/decorators/assert-or-warn.decorator';
|
||||
import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator';
|
||||
import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator';
|
||||
import { IsStrictlyLowerThan } from 'src/engine/core-modules/environment/decorators/is-strictly-lower-than.decorator';
|
||||
|
||||
@ -27,7 +27,7 @@ export class OnboardingService {
|
||||
|
||||
private async isSubscriptionIncompleteOnboardingStatus(user: User) {
|
||||
const hasSubscription =
|
||||
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccess(
|
||||
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||
user.defaultWorkspaceId,
|
||||
);
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ export class SSOService {
|
||||
);
|
||||
}
|
||||
const isSSOBillingEnabled =
|
||||
await this.billingService.verifyWorkspaceEntitlement(
|
||||
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||
workspaceId,
|
||||
this.featureLookUpKey,
|
||||
);
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
@ -42,6 +43,9 @@ registerEnumType(WorkspaceActivationStatus, {
|
||||
@UnPagedRelation('billingEntitlements', () => BillingEntitlement, {
|
||||
nullable: true,
|
||||
})
|
||||
@UnPagedRelation('billingCustomers', () => BillingCustomer, {
|
||||
nullable: true,
|
||||
})
|
||||
export class Workspace {
|
||||
@IDField(() => UUIDScalarType)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -121,6 +125,12 @@ export class Workspace {
|
||||
)
|
||||
billingSubscriptions: Relation<BillingSubscription[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => BillingCustomer,
|
||||
(billingCustomer) => billingCustomer.workspace,
|
||||
)
|
||||
billingCustomers: Relation<BillingCustomer[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => BillingEntitlement,
|
||||
(billingEntitlement) => billingEntitlement.workspace,
|
||||
|
||||
@ -133,7 +133,11 @@ export class WorkspaceResolver {
|
||||
@ResolveField(() => BillingSubscription, { nullable: true })
|
||||
async currentBillingSubscription(
|
||||
@Parent() workspace: Workspace,
|
||||
): Promise<BillingSubscription | null> {
|
||||
): Promise<BillingSubscription | undefined> {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId: workspace.id },
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user