Billing - fix duplicate customer in stripe + subscription constraint violation (#13091)
closes https://github.com/twentyhq/core-team-issues/issues/982
This commit is contained in:
@ -210,7 +210,7 @@ const testCases: {
|
|||||||
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
|
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: undefined },
|
||||||
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' },
|
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' },
|
||||||
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
|
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: AppPath.SignInUp },
|
||||||
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: undefined },
|
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace },
|
||||||
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
|
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile },
|
||||||
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
|
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.SYNC_EMAIL, res: AppPath.SyncEmails },
|
||||||
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
|
{ loc: AppPath.PlanRequiredSuccess, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.INVITE_TEAM, res: AppPath.InviteTeam },
|
||||||
|
|||||||
@ -92,7 +92,6 @@ export const usePageChangeEffectNavigateLocation = () => {
|
|||||||
onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION &&
|
onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION &&
|
||||||
!someMatchingLocationOf([
|
!someMatchingLocationOf([
|
||||||
AppPath.CreateWorkspace,
|
AppPath.CreateWorkspace,
|
||||||
AppPath.PlanRequiredSuccess,
|
|
||||||
AppPath.BookCallDecision,
|
AppPath.BookCallDecision,
|
||||||
AppPath.BookCall,
|
AppPath.BookCall,
|
||||||
])
|
])
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -8,16 +7,17 @@ import {
|
|||||||
SubscriptionInterval,
|
SubscriptionInterval,
|
||||||
useCheckoutSessionMutation,
|
useCheckoutSessionMutation,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
|
||||||
|
|
||||||
export const useHandleCheckoutSession = ({
|
export const useHandleCheckoutSession = ({
|
||||||
recurringInterval,
|
recurringInterval,
|
||||||
plan,
|
plan,
|
||||||
requirePaymentMethod,
|
requirePaymentMethod,
|
||||||
|
successUrlPath,
|
||||||
}: {
|
}: {
|
||||||
recurringInterval: SubscriptionInterval;
|
recurringInterval: SubscriptionInterval;
|
||||||
plan: BillingPlanKey;
|
plan: BillingPlanKey;
|
||||||
requirePaymentMethod: boolean;
|
requirePaymentMethod: boolean;
|
||||||
|
successUrlPath: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { redirect } = useRedirect();
|
const { redirect } = useRedirect();
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ export const useHandleCheckoutSession = ({
|
|||||||
const { data } = await checkoutSession({
|
const { data } = await checkoutSession({
|
||||||
variables: {
|
variables: {
|
||||||
recurringInterval,
|
recurringInterval,
|
||||||
successUrlPath: getSettingsPath(SettingsPath.Billing),
|
successUrlPath,
|
||||||
plan,
|
plan,
|
||||||
requirePaymentMethod,
|
requirePaymentMethod,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,14 +2,17 @@ import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/Bill
|
|||||||
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
|
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
|
||||||
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
import { InformationBanner } from '@/information-banner/components/InformationBanner';
|
||||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { SettingPermissionType } from '~/generated-metadata/graphql';
|
import { SettingPermissionType } from '~/generated-metadata/graphql';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
export const InformationBannerNoBillingSubscription = () => {
|
export const InformationBannerNoBillingSubscription = () => {
|
||||||
const { handleCheckoutSession, isSubmitting } = useHandleCheckoutSession({
|
const { handleCheckoutSession, isSubmitting } = useHandleCheckoutSession({
|
||||||
recurringInterval: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE.interval,
|
recurringInterval: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE.interval,
|
||||||
plan: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE.plan,
|
plan: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE.plan,
|
||||||
requirePaymentMethod: true,
|
requirePaymentMethod: true,
|
||||||
|
successUrlPath: getSettingsPath(SettingsPath.Billing),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { [SettingPermissionType.WORKSPACE]: hasPermissionToSubscribe } =
|
const { [SettingPermissionType.WORKSPACE]: hasPermissionToSubscribe } =
|
||||||
|
|||||||
@ -158,6 +158,7 @@ export const ChooseYourPlan = () => {
|
|||||||
recurringInterval: billingCheckoutSession.interval,
|
recurringInterval: billingCheckoutSession.interval,
|
||||||
plan: billingCheckoutSession.plan,
|
plan: billingCheckoutSession.plan,
|
||||||
requirePaymentMethod: billingCheckoutSession.requirePaymentMethod,
|
requirePaymentMethod: billingCheckoutSession.requirePaymentMethod,
|
||||||
|
successUrlPath: AppPath.PlanRequiredSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleTrialPeriodChange = (withCreditCard: boolean) => {
|
const handleTrialPeriodChange = (withCreditCard: boolean) => {
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
/* @license Enterprise */
|
/* @license Enterprise */
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
@ -18,6 +21,8 @@ export class StripeCheckoutService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly twentyConfigService: TwentyConfigService,
|
private readonly twentyConfigService: TwentyConfigService,
|
||||||
private readonly stripeSDKService: StripeSDKService,
|
private readonly stripeSDKService: StripeSDKService,
|
||||||
|
@InjectRepository(BillingCustomer, 'core')
|
||||||
|
private readonly billingCustomerRepository: Repository<BillingCustomer>,
|
||||||
) {
|
) {
|
||||||
if (!this.twentyConfigService.get('IS_BILLING_ENABLED')) {
|
if (!this.twentyConfigService.get('IS_BILLING_ENABLED')) {
|
||||||
return;
|
return;
|
||||||
@ -56,6 +61,11 @@ export class StripeCheckoutService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.billingCustomerRepository.save({
|
||||||
|
stripeCustomerId: customer.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
stripeCustomerId = customer.id;
|
stripeCustomerId = customer.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
/* @license Enterprise */
|
/* @license Enterprise */
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||||
import { StripeBillingMeterEventService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter-event.service';
|
import { StripeBillingMeterEventService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter-event.service';
|
||||||
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
|
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
|
||||||
import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service';
|
import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service';
|
||||||
@ -16,7 +18,11 @@ import { StripeSDKModule } from 'src/engine/core-modules/billing/stripe/stripe-s
|
|||||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DomainManagerModule, StripeSDKModule],
|
imports: [
|
||||||
|
DomainManagerModule,
|
||||||
|
StripeSDKModule,
|
||||||
|
TypeOrmModule.forFeature([BillingCustomer], 'core'),
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
StripeSubscriptionItemService,
|
StripeSubscriptionItemService,
|
||||||
StripeWebhookService,
|
StripeWebhookService,
|
||||||
|
|||||||
Reference in New Issue
Block a user