Add Integration and unit tests on Billing (#9317)
Solves [ https://github.com/twentyhq/private-issues/issues/214 ] **TLDR** Add unit and integration tests to Billing. First approach to run jest integration tests directly from VSCode. **In order to run the unit tests:** Run unit test using the CLI or with the jest extension directly from VSCode. **In order to run the integration tests:** Ensure that your database has the billingTables. If that's not the case, migrate the database with IS_BILLING_ENABLED set to true: ` npx nx run twenty-server:test:integration test/integration/billing/suites/billing-controller.integration-spec.ts` **Doing:** - Unit test on transformSubscriptionEventToSubscriptionItem - More tests cases in billingController integration tests. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
4ed1db3845
commit
c39af5f063
@ -96,7 +96,11 @@ module.exports = {
|
||||
rules: {},
|
||||
},
|
||||
{
|
||||
files: ['*.spec.@(ts|tsx|js|jsx)', '*.test.@(ts|tsx|js|jsx)'],
|
||||
files: [
|
||||
'*.spec.@(ts|tsx|js|jsx)',
|
||||
'*.integration-spec.@(ts|tsx|js|jsx)',
|
||||
'*.test.@(ts|tsx|js|jsx)',
|
||||
],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
|
||||
9
.github/workflows/ci-server.yaml
vendored
9
.github/workflows/ci-server.yaml
vendored
@ -184,6 +184,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/yarn-install
|
||||
- name: Update .env.test for billing
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
sed -i '$ a\
|
||||
IS_BILLING_ENABLED=true\
|
||||
BILLING_STRIPE_API_KEY=test-api-key\
|
||||
BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id\
|
||||
BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret' .env.test
|
||||
|
||||
- name: Server / Restore Task Cache
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: ./.github/workflows/actions/task-cache
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -49,4 +49,6 @@
|
||||
"files.associations": {
|
||||
".cursorrules": "markdown"
|
||||
},
|
||||
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}"
|
||||
}
|
||||
}
|
||||
|
||||
1
nx.json
1
nx.json
@ -14,6 +14,7 @@
|
||||
"!{projectRoot}/**/tsconfig.spec.json",
|
||||
"!{projectRoot}/**/*.test.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.spec.(ts|tsx)",
|
||||
"!{projectRoot}/**/*.integration-spec.ts",
|
||||
"!{projectRoot}/**/__tests__/*"
|
||||
],
|
||||
"production": [
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest';
|
||||
|
||||
const isBillingEnabled = process.env.IS_BILLING_ENABLED === 'true';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const tsConfig = require('./tsconfig.json');
|
||||
|
||||
@ -9,7 +10,9 @@ const jestConfig: JestConfigWithTsJest = {
|
||||
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||
rootDir: '.',
|
||||
testEnvironment: 'node',
|
||||
testRegex: '.integration-spec.ts$',
|
||||
testRegex: isBillingEnabled
|
||||
? 'integration-spec.ts'
|
||||
: '^(?!.*billing).*\\.integration-spec\\.ts$',
|
||||
modulePathIgnorePatterns: ['<rootDir>/dist'],
|
||||
globalSetup: '<rootDir>/test/integration/utils/setup-test.ts',
|
||||
globalTeardown: '<rootDir>/test/integration/utils/teardown-test.ts',
|
||||
|
||||
@ -10,26 +10,27 @@ import {
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { Response } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
|
||||
import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
|
||||
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
|
||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service';
|
||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service';
|
||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
|
||||
@Controller('billing')
|
||||
@UseFilters(BillingRestApiExceptionFilter)
|
||||
export class BillingController {
|
||||
protected readonly logger = new Logger(BillingController.name);
|
||||
|
||||
constructor(
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeWebhookService: StripeWebhookService,
|
||||
private readonly billingWebhookSubscriptionService: BillingWebhookSubscriptionService,
|
||||
private readonly billingWebhookEntitlementService: BillingWebhookEntitlementService,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
@ -48,72 +49,63 @@ export class BillingController {
|
||||
|
||||
return;
|
||||
}
|
||||
const event = this.stripeService.constructEventFromPayload(
|
||||
const event = this.stripeWebhookService.constructEventFromPayload(
|
||||
signature,
|
||||
req.rawBody,
|
||||
);
|
||||
|
||||
if (event.type === WebhookEvent.SETUP_INTENT_SUCCEEDED) {
|
||||
await this.billingSubscriptionService.handleUnpaidInvoices(event.data);
|
||||
}
|
||||
try {
|
||||
const result = await this.handleStripeEvent(event);
|
||||
|
||||
if (
|
||||
event.type === WebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED ||
|
||||
event.type === WebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED ||
|
||||
event.type === WebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED
|
||||
) {
|
||||
const workspaceId = event.data.object.metadata?.workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
res.status(200).send(result).end();
|
||||
} catch (error) {
|
||||
if (error instanceof BillingException) {
|
||||
res.status(404).end();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.billingWebhookSubscriptionService.processStripeEvent(
|
||||
workspaceId,
|
||||
event.data,
|
||||
);
|
||||
}
|
||||
if (
|
||||
event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED
|
||||
) {
|
||||
try {
|
||||
await this.billingWebhookEntitlementService.processStripeEvent(
|
||||
}
|
||||
|
||||
private async handleStripeEvent(event: Stripe.Event) {
|
||||
switch (event.type) {
|
||||
case BillingWebhookEvent.SETUP_INTENT_SUCCEEDED:
|
||||
return await this.billingSubscriptionService.handleUnpaidInvoices(
|
||||
event.data,
|
||||
);
|
||||
case BillingWebhookEvent.PRICE_UPDATED:
|
||||
case BillingWebhookEvent.PRICE_CREATED:
|
||||
return await this.billingWebhookPriceService.processStripeEvent(
|
||||
event.data,
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BillingException &&
|
||||
error.code === BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND
|
||||
) {
|
||||
res.status(404).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === WebhookEvent.PRODUCT_CREATED ||
|
||||
event.type === WebhookEvent.PRODUCT_UPDATED
|
||||
) {
|
||||
await this.billingWebhookProductService.processStripeEvent(event.data);
|
||||
}
|
||||
if (
|
||||
event.type === WebhookEvent.PRICE_CREATED ||
|
||||
event.type === WebhookEvent.PRICE_UPDATED
|
||||
) {
|
||||
try {
|
||||
await this.billingWebhookPriceService.processStripeEvent(event.data);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof BillingException &&
|
||||
error.code === BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND
|
||||
) {
|
||||
res.status(404).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
case BillingWebhookEvent.PRODUCT_UPDATED:
|
||||
case BillingWebhookEvent.PRODUCT_CREATED:
|
||||
return await this.billingWebhookProductService.processStripeEvent(
|
||||
event.data,
|
||||
);
|
||||
case BillingWebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED:
|
||||
return await this.billingWebhookEntitlementService.processStripeEvent(
|
||||
event.data,
|
||||
);
|
||||
|
||||
res.status(200).end();
|
||||
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_CREATED:
|
||||
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_UPDATED:
|
||||
case BillingWebhookEvent.CUSTOMER_SUBSCRIPTION_DELETED: {
|
||||
const workspaceId = event.data.object.metadata?.workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new BillingException(
|
||||
'Workspace ID is required for subscription events',
|
||||
BillingExceptionCode.BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.billingWebhookSubscriptionService.processStripeEvent(
|
||||
workspaceId,
|
||||
event.data,
|
||||
);
|
||||
}
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,4 +12,5 @@ export class BillingException extends CustomException {
|
||||
export enum BillingExceptionCode {
|
||||
BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND',
|
||||
BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND',
|
||||
BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND',
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
@ -23,12 +23,13 @@ export class BillingResolver {
|
||||
constructor(
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
) {}
|
||||
|
||||
@Query(() => ProductPricesEntity)
|
||||
async getProductPrices(@Args() { product }: ProductInput) {
|
||||
const productPrices = await this.stripeService.getStripePrices(product);
|
||||
const productPrices =
|
||||
await this.stripePriceService.getStripePrices(product);
|
||||
|
||||
return {
|
||||
totalNumberOfPrices: productPrices.length,
|
||||
@ -63,7 +64,7 @@ export class BillingResolver {
|
||||
requirePaymentMethod,
|
||||
}: CheckoutSessionInput,
|
||||
) {
|
||||
const productPrice = await this.stripeService.getStripePrice(
|
||||
const productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
recurringInterval,
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
interface SyncCustomerDataCommandOptions
|
||||
@ -23,7 +23,7 @@ export class BillingSyncCustomerDataCommand extends ActiveWorkspacesCommandRunne
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeSubscriptionService: StripeSubscriptionService,
|
||||
@InjectRepository(BillingCustomer, 'core')
|
||||
protected readonly billingCustomerRepository: Repository<BillingCustomer>,
|
||||
) {
|
||||
@ -71,7 +71,7 @@ export class BillingSyncCustomerDataCommand extends ActiveWorkspacesCommandRunne
|
||||
|
||||
if (!options.dryRun && !billingCustomer) {
|
||||
const stripeCustomerId =
|
||||
await this.stripeService.getStripeCustomerIdFromWorkspaceId(
|
||||
await this.stripeSubscriptionService.getStripeCustomerIdFromWorkspaceId(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@ import {
|
||||
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 { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
|
||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service';
|
||||
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
|
||||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
|
||||
import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util';
|
||||
@ -30,7 +32,9 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
|
||||
private readonly billingProductRepository: Repository<BillingProduct>,
|
||||
@InjectRepository(BillingMeter, 'core')
|
||||
private readonly billingMeterRepository: Repository<BillingMeter>,
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeBillingMeterService: StripeBillingMeterService,
|
||||
private readonly stripeProductService: StripeProductService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -92,7 +96,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
|
||||
}
|
||||
await this.upsertProductRepositoryData(product, options);
|
||||
|
||||
const prices = await this.stripeService.getPricesByProductId(
|
||||
const prices = await this.stripePriceService.getPricesByProductId(
|
||||
product.id,
|
||||
);
|
||||
|
||||
@ -133,11 +137,11 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
|
||||
passedParams: string[],
|
||||
options: BaseCommandOptions,
|
||||
): Promise<void> {
|
||||
const billingMeters = await this.stripeService.getAllMeters();
|
||||
const billingMeters = await this.stripeBillingMeterService.getAllMeters();
|
||||
|
||||
await this.upsertMetersRepositoryData(billingMeters, options);
|
||||
|
||||
const billingProducts = await this.stripeService.getAllProducts();
|
||||
const billingProducts = await this.stripeProductService.getAllProducts();
|
||||
|
||||
const billingPrices = await this.processBillingPricesByProductBatches(
|
||||
billingProducts,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export enum WebhookEvent {
|
||||
export enum BillingWebhookEvent {
|
||||
CUSTOMER_SUBSCRIPTION_CREATED = 'customer.subscription.created',
|
||||
CUSTOMER_SUBSCRIPTION_UPDATED = 'customer.subscription.updated',
|
||||
CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted',
|
||||
|
||||
@ -26,6 +26,12 @@ export class BillingRestApiExceptionFilter implements ExceptionFilter {
|
||||
response,
|
||||
404,
|
||||
);
|
||||
case BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
404,
|
||||
);
|
||||
default:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.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';
|
||||
@ -18,7 +18,7 @@ export class UpdateSubscriptionQuantityJob {
|
||||
|
||||
constructor(
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
@ -41,7 +41,7 @@ export class UpdateSubscriptionQuantityJob {
|
||||
data.workspaceId,
|
||||
);
|
||||
|
||||
await this.stripeService.updateSubscriptionItem(
|
||||
await this.stripeSubscriptionItemService.updateSubscriptionItem(
|
||||
billingSubscriptionItem.stripeSubscriptionItemId,
|
||||
workspaceMembersCount,
|
||||
);
|
||||
|
||||
@ -6,7 +6,8 @@ import { Repository } from 'typeorm';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service';
|
||||
import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
@ -17,7 +18,8 @@ import { assert } from 'src/utils/assert';
|
||||
export class BillingPortalWorkspaceService {
|
||||
protected readonly logger = new Logger(BillingPortalWorkspaceService.name);
|
||||
constructor(
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeCheckoutService: StripeCheckoutService,
|
||||
private readonly stripeBillingPortalService: StripeBillingPortalService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
@ -52,7 +54,7 @@ export class BillingPortalWorkspaceService {
|
||||
})
|
||||
)?.stripeCustomerId;
|
||||
|
||||
const session = await this.stripeService.createCheckoutSession(
|
||||
const session = await this.stripeCheckoutService.createCheckoutSession(
|
||||
user,
|
||||
workspace.id,
|
||||
priceId,
|
||||
@ -97,10 +99,11 @@ export class BillingPortalWorkspaceService {
|
||||
}
|
||||
const returnUrl = frontBaseUrl.toString();
|
||||
|
||||
const session = await this.stripeService.createBillingPortalSession(
|
||||
stripeCustomerId,
|
||||
returnUrl,
|
||||
);
|
||||
const session =
|
||||
await this.stripeBillingPortalService.createBillingPortalSession(
|
||||
stripeCustomerId,
|
||||
returnUrl,
|
||||
);
|
||||
|
||||
assert(session.url, 'Error: missing billingPortal.session.url');
|
||||
|
||||
|
||||
@ -12,7 +12,9 @@ import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.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 { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@ -20,7 +22,9 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
export class BillingSubscriptionService {
|
||||
protected readonly logger = new Logger(BillingSubscriptionService.name);
|
||||
constructor(
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeSubscriptionService: StripeSubscriptionService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
@InjectRepository(BillingEntitlement, 'core')
|
||||
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
||||
@ -78,7 +82,7 @@ export class BillingSubscriptionService {
|
||||
});
|
||||
|
||||
if (subscriptionToCancel) {
|
||||
await this.stripeService.cancelSubscription(
|
||||
await this.stripeSubscriptionService.cancelSubscription(
|
||||
subscriptionToCancel.stripeSubscriptionId,
|
||||
);
|
||||
await this.billingSubscriptionRepository.delete(subscriptionToCancel.id);
|
||||
@ -91,10 +95,15 @@ export class BillingSubscriptionService {
|
||||
);
|
||||
|
||||
if (billingSubscription?.status === 'unpaid') {
|
||||
await this.stripeService.collectLastInvoice(
|
||||
await this.stripeSubscriptionService.collectLastInvoice(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
handleUnpaidInvoiceStripeSubscriptionId:
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
};
|
||||
}
|
||||
|
||||
async getWorkspaceEntitlementByKey(
|
||||
@ -127,7 +136,7 @@ export class BillingSubscriptionService {
|
||||
const billingSubscriptionItem =
|
||||
await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id);
|
||||
|
||||
const productPrice = await this.stripeService.getStripePrice(
|
||||
const productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
newInterval,
|
||||
);
|
||||
@ -138,7 +147,7 @@ export class BillingSubscriptionService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.stripeService.updateBillingSubscriptionItem(
|
||||
await this.stripeSubscriptionItemService.updateBillingSubscriptionItem(
|
||||
billingSubscriptionItem,
|
||||
productPrice.stripePriceId,
|
||||
);
|
||||
|
||||
@ -48,5 +48,9 @@ export class BillingWebhookEntitlementService {
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
stripeEntitlementCustomerId: data.object.customer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,14 +11,14 @@ import {
|
||||
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 { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
|
||||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
|
||||
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util';
|
||||
@Injectable()
|
||||
export class BillingWebhookPriceService {
|
||||
protected readonly logger = new Logger(BillingWebhookPriceService.name);
|
||||
constructor(
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeBillingMeterService: StripeBillingMeterService,
|
||||
@InjectRepository(BillingPrice, 'core')
|
||||
private readonly billingPriceRepository: Repository<BillingPrice>,
|
||||
@InjectRepository(BillingMeter, 'core')
|
||||
@ -45,7 +45,7 @@ export class BillingWebhookPriceService {
|
||||
const meterId = data.object.recurring?.meter;
|
||||
|
||||
if (meterId) {
|
||||
const meterData = await this.stripeService.getMeter(meterId);
|
||||
const meterData = await this.stripeBillingMeterService.getMeter(meterId);
|
||||
|
||||
await this.billingMeterRepository.upsert(
|
||||
transformStripeMeterDataToMeterRepositoryData(meterData),
|
||||
@ -63,5 +63,10 @@ export class BillingWebhookPriceService {
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
stripePriceId: data.object.id,
|
||||
stripeMeterId: meterId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,10 @@ export class BillingWebhookProductService {
|
||||
conflictPaths: ['stripeProductId'],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
});
|
||||
|
||||
return {
|
||||
stripeProductId: data.object.id,
|
||||
};
|
||||
}
|
||||
|
||||
isStripeValidProductMetadata(
|
||||
|
||||
@ -8,7 +8,7 @@ import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billin
|
||||
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 { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||
import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util';
|
||||
import { transformStripeSubscriptionEventToSubscriptionItemRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util';
|
||||
import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util';
|
||||
@ -22,7 +22,7 @@ export class BillingWebhookSubscriptionService {
|
||||
BillingWebhookSubscriptionService.name,
|
||||
);
|
||||
constructor(
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly stripeCustomerService: StripeCustomerService,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
@InjectRepository(BillingSubscriptionItem, 'core')
|
||||
@ -45,7 +45,7 @@ export class BillingWebhookSubscriptionService {
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
return;
|
||||
return { noWorkspace: true };
|
||||
}
|
||||
|
||||
await this.billingCustomerRepository.upsert(
|
||||
@ -106,9 +106,14 @@ export class BillingWebhookSubscriptionService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.stripeService.updateCustomerMetadataWorkspaceId(
|
||||
await this.stripeCustomerService.updateCustomerMetadataWorkspaceId(
|
||||
String(data.object.customer),
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
stripeSubscriptionId: data.object.id,
|
||||
stripeCustomerId: data.object.customer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeBillingMeterService {
|
||||
protected readonly logger = new Logger(StripeBillingMeterService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async getMeter(stripeMeterId: string) {
|
||||
return await this.stripe.billing.meters.retrieve(stripeMeterId);
|
||||
}
|
||||
|
||||
async getAllMeters() {
|
||||
const meters = await this.stripe.billing.meters.list();
|
||||
|
||||
return meters.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeBillingPortalService {
|
||||
protected readonly logger = new Logger(StripeBillingPortalService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async createBillingPortalSession(
|
||||
stripeCustomerId: string,
|
||||
returnUrl?: string,
|
||||
): Promise<Stripe.BillingPortal.Session> {
|
||||
return await this.stripe.billingPortal.sessions.create({
|
||||
customer: stripeCustomerId,
|
||||
return_url:
|
||||
returnUrl ?? this.domainManagerService.getBaseUrl().toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class StripeCheckoutService {
|
||||
protected readonly logger = new Logger(StripeCheckoutService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async createCheckoutSession(
|
||||
user: User,
|
||||
workspaceId: string,
|
||||
priceId: string,
|
||||
quantity: number,
|
||||
successUrl?: string,
|
||||
cancelUrl?: string,
|
||||
stripeCustomerId?: string,
|
||||
plan: BillingPlanKey = BillingPlanKey.PRO,
|
||||
requirePaymentMethod = true,
|
||||
): Promise<Stripe.Checkout.Session> {
|
||||
return await this.stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
workspaceId,
|
||||
plan,
|
||||
},
|
||||
trial_period_days: this.environmentService.get(
|
||||
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
|
||||
),
|
||||
},
|
||||
automatic_tax: { enabled: !!requirePaymentMethod },
|
||||
tax_id_collection: { enabled: !!requirePaymentMethod },
|
||||
customer: stripeCustomerId,
|
||||
customer_update: stripeCustomerId ? { name: 'auto' } : undefined,
|
||||
customer_email: stripeCustomerId ? undefined : user.email,
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
payment_method_collection: requirePaymentMethod
|
||||
? 'always'
|
||||
: 'if_required',
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeCustomerService {
|
||||
protected readonly logger = new Logger(StripeCustomerService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async updateCustomerMetadataWorkspaceId(
|
||||
stripeCustomerId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.stripe.customers.update(stripeCustomerId, {
|
||||
metadata: { workspaceId: workspaceId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripePriceService {
|
||||
protected readonly logger = new Logger(StripePriceService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async getStripePrices(product: AvailableProduct) {
|
||||
const stripeProductId = this.getStripeProductId(product);
|
||||
|
||||
const prices = await this.stripe.prices.search({
|
||||
query: `product: '${stripeProductId}'`,
|
||||
});
|
||||
|
||||
return this.formatProductPrices(prices.data);
|
||||
}
|
||||
|
||||
async getStripePrice(product: AvailableProduct, recurringInterval: string) {
|
||||
const productPrices = await this.getStripePrices(product);
|
||||
|
||||
return productPrices.find(
|
||||
(price) => price.recurringInterval === recurringInterval,
|
||||
);
|
||||
}
|
||||
|
||||
getStripeProductId(product: AvailableProduct) {
|
||||
if (product === AvailableProduct.BasePlan) {
|
||||
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
|
||||
}
|
||||
} // PD:,will be eliminated after refactoring
|
||||
|
||||
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
|
||||
const productPrices: ProductPriceEntity[] = Object.values(
|
||||
prices
|
||||
.filter((item) => item.recurring?.interval && item.unit_amount)
|
||||
.reduce((acc, item: Stripe.Price) => {
|
||||
const interval = item.recurring?.interval;
|
||||
|
||||
if (!interval || !item.unit_amount) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[interval] || item.created > acc[interval].created) {
|
||||
acc[interval] = {
|
||||
unitAmount: item.unit_amount,
|
||||
recurringInterval: interval,
|
||||
created: item.created,
|
||||
stripePriceId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
return acc satisfies Record<string, ProductPriceEntity>;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
return productPrices.sort((a, b) => a.unitAmount - b.unitAmount);
|
||||
}
|
||||
|
||||
async getPricesByProductId(productId: string) {
|
||||
const prices = await this.stripe.prices.search({
|
||||
query: `product:'${productId}'`,
|
||||
});
|
||||
|
||||
return prices.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeProductService {
|
||||
protected readonly logger = new Logger(StripeProductService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async getAllProducts() {
|
||||
const products = await this.stripe.products.list();
|
||||
|
||||
return products.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeSubscriptionItemService {
|
||||
protected readonly logger = new Logger(StripeSubscriptionItemService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async updateSubscriptionItem(stripeItemId: string, quantity: number) {
|
||||
await this.stripe.subscriptionItems.update(stripeItemId, { quantity });
|
||||
}
|
||||
|
||||
async updateBillingSubscriptionItem(
|
||||
stripeSubscriptionItem: BillingSubscriptionItem,
|
||||
stripePriceId: string,
|
||||
) {
|
||||
await this.stripe.subscriptionItems.update(
|
||||
stripeSubscriptionItem.stripeSubscriptionItemId,
|
||||
{
|
||||
price: stripePriceId,
|
||||
quantity:
|
||||
stripeSubscriptionItem.quantity === null
|
||||
? undefined
|
||||
: stripeSubscriptionItem.quantity,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeSubscriptionService {
|
||||
protected readonly logger = new Logger(StripeSubscriptionService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async cancelSubscription(stripeSubscriptionId: string) {
|
||||
await this.stripe.subscriptions.cancel(stripeSubscriptionId);
|
||||
}
|
||||
|
||||
async getStripeCustomerIdFromWorkspaceId(workspaceId: string) {
|
||||
const subscription = await this.stripe.subscriptions.search({
|
||||
query: `metadata['workspaceId']:'${workspaceId}'`,
|
||||
limit: 1,
|
||||
});
|
||||
const stripeCustomerId = subscription.data[0].customer
|
||||
? String(subscription.data[0].customer)
|
||||
: undefined;
|
||||
|
||||
return stripeCustomerId;
|
||||
}
|
||||
|
||||
async collectLastInvoice(stripeSubscriptionId: string) {
|
||||
const subscription = await this.stripe.subscriptions.retrieve(
|
||||
stripeSubscriptionId,
|
||||
{ expand: ['latest_invoice'] },
|
||||
);
|
||||
const latestInvoice = subscription.latest_invoice;
|
||||
|
||||
if (
|
||||
!(
|
||||
latestInvoice &&
|
||||
typeof latestInvoice !== 'string' &&
|
||||
latestInvoice.status === 'draft'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await this.stripe.invoices.pay(latestInvoice.id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
@Injectable()
|
||||
export class StripeWebhookService {
|
||||
protected readonly logger = new Logger(StripeWebhookService.name);
|
||||
private stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeSDKService: StripeSDKService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = this.stripeSDKService.getStripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
constructEventFromPayload(signature: string, payload: Buffer) {
|
||||
const webhookSecret = this.environmentService.get(
|
||||
'BILLING_STRIPE_WEBHOOK_SECRET',
|
||||
);
|
||||
|
||||
const returnValue = this.stripe.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
webhookSecret,
|
||||
);
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeSDKMock } from 'src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk.mock';
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeSDKMockService implements StripeSDKService {
|
||||
getStripe(stripeApiKey: string) {
|
||||
return new StripeSDKMock(stripeApiKey) as unknown as Stripe;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export class StripeSDKMock {
|
||||
constructor(private readonly apiKey: string) {}
|
||||
|
||||
customers = {
|
||||
update: (_id: string, _params?: Stripe.CustomerUpdateParams) => {
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
webhooks = {
|
||||
constructEvent: (
|
||||
payload: Buffer,
|
||||
signature: string,
|
||||
_webhookSecret: string,
|
||||
) => {
|
||||
if (signature === 'correct-signature') {
|
||||
const body = JSON.parse(payload.toString());
|
||||
|
||||
return {
|
||||
type: body.type,
|
||||
data: body.data,
|
||||
};
|
||||
}
|
||||
throw new Error('Invalid signature');
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
@Injectable()
|
||||
export class StripeSDKService {
|
||||
getStripe(stripeApiKey: string) {
|
||||
return new Stripe(stripeApiKey, {});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
|
||||
@Module({
|
||||
providers: [StripeSDKService],
|
||||
exports: [StripeSDKService],
|
||||
})
|
||||
export class StripeSDKModule {}
|
||||
@ -1,11 +1,40 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.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 { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service';
|
||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service';
|
||||
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
|
||||
import { StripeSDKModule } from 'src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module';
|
||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [DomainManagerModule],
|
||||
providers: [StripeService],
|
||||
exports: [StripeService],
|
||||
imports: [DomainManagerModule, StripeSDKModule],
|
||||
providers: [
|
||||
StripeSubscriptionItemService,
|
||||
StripeWebhookService,
|
||||
StripeCheckoutService,
|
||||
StripeSubscriptionService,
|
||||
StripeBillingPortalService,
|
||||
StripeBillingMeterService,
|
||||
StripeCustomerService,
|
||||
StripePriceService,
|
||||
StripeProductService,
|
||||
],
|
||||
exports: [
|
||||
StripeWebhookService,
|
||||
StripeBillingPortalService,
|
||||
StripeBillingMeterService,
|
||||
StripeCustomerService,
|
||||
StripePriceService,
|
||||
StripeCheckoutService,
|
||||
StripeSubscriptionItemService,
|
||||
StripeSubscriptionService,
|
||||
StripeProductService,
|
||||
],
|
||||
})
|
||||
export class StripeModule {}
|
||||
|
||||
@ -1,233 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class StripeService {
|
||||
protected readonly logger = new Logger(StripeService.name);
|
||||
private readonly stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
this.stripe = new Stripe(
|
||||
this.environmentService.get('BILLING_STRIPE_API_KEY'),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
constructEventFromPayload(signature: string, payload: Buffer) {
|
||||
const webhookSecret = this.environmentService.get(
|
||||
'BILLING_STRIPE_WEBHOOK_SECRET',
|
||||
);
|
||||
|
||||
return this.stripe.webhooks.constructEvent(
|
||||
payload,
|
||||
signature,
|
||||
webhookSecret,
|
||||
);
|
||||
}
|
||||
|
||||
async getStripePrices(product: AvailableProduct) {
|
||||
const stripeProductId = this.getStripeProductId(product);
|
||||
|
||||
const prices = await this.stripe.prices.search({
|
||||
query: `product: '${stripeProductId}'`,
|
||||
});
|
||||
|
||||
return this.formatProductPrices(prices.data);
|
||||
}
|
||||
|
||||
async getStripePrice(product: AvailableProduct, recurringInterval: string) {
|
||||
const productPrices = await this.getStripePrices(product);
|
||||
|
||||
return productPrices.find(
|
||||
(price) => price.recurringInterval === recurringInterval,
|
||||
);
|
||||
}
|
||||
|
||||
getStripeProductId(product: AvailableProduct) {
|
||||
if (product === AvailableProduct.BasePlan) {
|
||||
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
|
||||
}
|
||||
}
|
||||
|
||||
async updateSubscriptionItem(stripeItemId: string, quantity: number) {
|
||||
await this.stripe.subscriptionItems.update(stripeItemId, { quantity });
|
||||
}
|
||||
|
||||
async cancelSubscription(stripeSubscriptionId: string) {
|
||||
await this.stripe.subscriptions.cancel(stripeSubscriptionId);
|
||||
}
|
||||
|
||||
async createBillingPortalSession(
|
||||
stripeCustomerId: string,
|
||||
returnUrl?: string,
|
||||
): Promise<Stripe.BillingPortal.Session> {
|
||||
return await this.stripe.billingPortal.sessions.create({
|
||||
customer: stripeCustomerId,
|
||||
return_url:
|
||||
returnUrl ?? this.domainManagerService.getBaseUrl().toString(),
|
||||
});
|
||||
}
|
||||
|
||||
async createCheckoutSession(
|
||||
user: User,
|
||||
workspaceId: string,
|
||||
priceId: string,
|
||||
quantity: number,
|
||||
successUrl?: string,
|
||||
cancelUrl?: string,
|
||||
stripeCustomerId?: string,
|
||||
plan: BillingPlanKey = BillingPlanKey.PRO,
|
||||
requirePaymentMethod = true,
|
||||
): Promise<Stripe.Checkout.Session> {
|
||||
return await this.stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity,
|
||||
},
|
||||
],
|
||||
mode: 'subscription',
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
workspaceId,
|
||||
plan,
|
||||
},
|
||||
trial_period_days: this.environmentService.get(
|
||||
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
|
||||
),
|
||||
},
|
||||
automatic_tax: { enabled: !!requirePaymentMethod },
|
||||
tax_id_collection: { enabled: !!requirePaymentMethod },
|
||||
customer: stripeCustomerId,
|
||||
customer_update: stripeCustomerId ? { name: 'auto' } : undefined,
|
||||
customer_email: stripeCustomerId ? undefined : user.email,
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
payment_method_collection: requirePaymentMethod
|
||||
? 'always'
|
||||
: 'if_required',
|
||||
});
|
||||
}
|
||||
|
||||
async collectLastInvoice(stripeSubscriptionId: string) {
|
||||
const subscription = await this.stripe.subscriptions.retrieve(
|
||||
stripeSubscriptionId,
|
||||
{ expand: ['latest_invoice'] },
|
||||
);
|
||||
const latestInvoice = subscription.latest_invoice;
|
||||
|
||||
if (
|
||||
!(
|
||||
latestInvoice &&
|
||||
typeof latestInvoice !== 'string' &&
|
||||
latestInvoice.status === 'draft'
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await this.stripe.invoices.pay(latestInvoice.id);
|
||||
}
|
||||
|
||||
async updateBillingSubscriptionItem(
|
||||
stripeSubscriptionItem: BillingSubscriptionItem,
|
||||
stripePriceId: string,
|
||||
) {
|
||||
await this.stripe.subscriptionItems.update(
|
||||
stripeSubscriptionItem.stripeSubscriptionItemId,
|
||||
{
|
||||
price: stripePriceId,
|
||||
quantity:
|
||||
stripeSubscriptionItem.quantity === null
|
||||
? undefined
|
||||
: stripeSubscriptionItem.quantity,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async updateCustomerMetadataWorkspaceId(
|
||||
stripeCustomerId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.stripe.customers.update(stripeCustomerId, {
|
||||
metadata: { workspaceId: workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async getMeter(stripeMeterId: string) {
|
||||
return await this.stripe.billing.meters.retrieve(stripeMeterId);
|
||||
}
|
||||
|
||||
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
|
||||
const productPrices: ProductPriceEntity[] = Object.values(
|
||||
prices
|
||||
.filter((item) => item.recurring?.interval && item.unit_amount)
|
||||
.reduce((acc, item: Stripe.Price) => {
|
||||
const interval = item.recurring?.interval;
|
||||
|
||||
if (!interval || !item.unit_amount) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[interval] || item.created > acc[interval].created) {
|
||||
acc[interval] = {
|
||||
unitAmount: item.unit_amount,
|
||||
recurringInterval: interval,
|
||||
created: item.created,
|
||||
stripePriceId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
return acc satisfies Record<string, ProductPriceEntity>;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
return productPrices.sort((a, b) => a.unitAmount - b.unitAmount);
|
||||
}
|
||||
|
||||
async getStripeCustomerIdFromWorkspaceId(workspaceId: string) {
|
||||
const subscription = await this.stripe.subscriptions.search({
|
||||
query: `metadata['workspaceId']:'${workspaceId}'`,
|
||||
limit: 1,
|
||||
});
|
||||
const stripeCustomerId = subscription.data[0].customer
|
||||
? String(subscription.data[0].customer)
|
||||
: undefined;
|
||||
|
||||
return stripeCustomerId;
|
||||
}
|
||||
|
||||
async getAllProducts() {
|
||||
const products = await this.stripe.products.list();
|
||||
|
||||
return products.data;
|
||||
}
|
||||
|
||||
async getPricesByProductId(productId: string) {
|
||||
const prices = await this.stripe.prices.search({
|
||||
query: `product:'${productId}'`,
|
||||
});
|
||||
|
||||
return prices.data;
|
||||
}
|
||||
|
||||
async getAllMeters() {
|
||||
const meters = await this.stripe.billing.meters.list();
|
||||
|
||||
return meters.data;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
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';
|
||||
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
|
||||
describe('isStripeValidProductMetadata', () => {
|
||||
it('should return true if metadata is empty', () => {
|
||||
const metadata: Stripe.Metadata = {};
|
||||
|
||||
expect(isStripeValidProductMetadata(metadata)).toBe(true);
|
||||
});
|
||||
it('should return true if metadata has the correct keys with correct values', () => {
|
||||
const metadata: Stripe.Metadata = {
|
||||
planKey: BillingPlanKey.PRO,
|
||||
priceUsageBased: BillingUsageType.METERED,
|
||||
};
|
||||
|
||||
expect(isStripeValidProductMetadata(metadata)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if metadata has extra keys', () => {
|
||||
const metadata: Stripe.Metadata = {
|
||||
planKey: BillingPlanKey.ENTERPRISE,
|
||||
priceUsageBased: BillingUsageType.METERED,
|
||||
randomKey: 'randomValue',
|
||||
};
|
||||
|
||||
expect(isStripeValidProductMetadata(metadata)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if metadata has invalid keys', () => {
|
||||
const metadata: Stripe.Metadata = {
|
||||
planKey: 'invalid',
|
||||
priceUsageBased: BillingUsageType.METERED,
|
||||
};
|
||||
|
||||
expect(isStripeValidProductMetadata(metadata)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if metadata has invalid values', () => {
|
||||
const metadata: Stripe.Metadata = {
|
||||
planKey: BillingPlanKey.PRO,
|
||||
priceUsageBased: 'invalid',
|
||||
};
|
||||
|
||||
expect(isStripeValidProductMetadata(metadata)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the metadata does not have the required keys', () => {
|
||||
const metadata: Stripe.Metadata = {
|
||||
randomKey: 'randomValue',
|
||||
};
|
||||
|
||||
expect(isStripeValidProductMetadata(metadata)).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,84 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util';
|
||||
|
||||
describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', () => {
|
||||
it('should return the SSO key with true value', () => {
|
||||
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
|
||||
object: {
|
||||
customer: 'cus_123',
|
||||
entitlements: {
|
||||
data: [
|
||||
{
|
||||
lookup_key: 'SSO',
|
||||
feature: 'SSO',
|
||||
livemode: false,
|
||||
id: 'ent_123',
|
||||
object: 'entitlements.active_entitlement',
|
||||
},
|
||||
],
|
||||
object: 'list',
|
||||
has_more: false,
|
||||
url: '',
|
||||
},
|
||||
livemode: false,
|
||||
object: 'entitlements.active_entitlement_summary',
|
||||
},
|
||||
};
|
||||
|
||||
const result =
|
||||
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
|
||||
'workspaceId',
|
||||
data,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
workspaceId: 'workspaceId',
|
||||
key: BillingEntitlementKey.SSO,
|
||||
value: true,
|
||||
stripeCustomerId: 'cus_123',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the SSO key with false value,should only render the values that are listed in BillingEntitlementKeys', () => {
|
||||
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
|
||||
object: {
|
||||
customer: 'cus_123',
|
||||
entitlements: {
|
||||
data: [
|
||||
{
|
||||
id: 'ent_123',
|
||||
object: 'entitlements.active_entitlement',
|
||||
lookup_key: 'DIFFERENT_KEY',
|
||||
feature: 'DIFFERENT_FEATURE',
|
||||
livemode: false,
|
||||
},
|
||||
],
|
||||
object: 'list',
|
||||
has_more: false,
|
||||
url: '',
|
||||
},
|
||||
livemode: false,
|
||||
object: 'entitlements.active_entitlement_summary',
|
||||
},
|
||||
};
|
||||
|
||||
const result =
|
||||
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
|
||||
'workspaceId',
|
||||
data,
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
workspaceId: 'workspaceId',
|
||||
key: BillingEntitlementKey.SSO,
|
||||
value: false,
|
||||
stripeCustomerId: 'cus_123',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,94 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
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';
|
||||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
|
||||
|
||||
describe('transformStripeMeterDataToMeterRepositoryData', () => {
|
||||
it('should return the correct data with customer mapping', () => {
|
||||
const data: Stripe.Billing.Meter = {
|
||||
id: 'met_123',
|
||||
object: 'billing.meter',
|
||||
created: 1719859200,
|
||||
display_name: 'Meter 1',
|
||||
event_name: 'event_1',
|
||||
status: 'active',
|
||||
customer_mapping: {
|
||||
event_payload_key: 'event_payload_key_1',
|
||||
type: 'by_id',
|
||||
},
|
||||
default_aggregation: {
|
||||
formula: 'count',
|
||||
},
|
||||
event_time_window: 'day',
|
||||
livemode: false,
|
||||
status_transitions: {
|
||||
deactivated_at: null,
|
||||
},
|
||||
updated: 1719859200,
|
||||
value_settings: {
|
||||
event_payload_key: 'event_payload_key_1',
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformStripeMeterDataToMeterRepositoryData(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripeMeterId: 'met_123',
|
||||
displayName: 'Meter 1',
|
||||
eventName: 'event_1',
|
||||
status: BillingMeterStatus.ACTIVE,
|
||||
customerMapping: {
|
||||
event_payload_key: 'event_payload_key_1',
|
||||
type: 'by_id',
|
||||
},
|
||||
eventTimeWindow: BillingMeterEventTimeWindow.DAY,
|
||||
valueSettings: {
|
||||
event_payload_key: 'event_payload_key_1',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('should return the correct data with null values', () => {
|
||||
const data: Stripe.Billing.Meter = {
|
||||
id: 'met_1234',
|
||||
object: 'billing.meter',
|
||||
created: 1719859200,
|
||||
display_name: 'Meter 2',
|
||||
event_name: 'event_2',
|
||||
status: 'inactive',
|
||||
customer_mapping: {
|
||||
event_payload_key: 'event_payload_key_2',
|
||||
type: 'by_id',
|
||||
},
|
||||
default_aggregation: {
|
||||
formula: 'sum',
|
||||
},
|
||||
event_time_window: null,
|
||||
livemode: false,
|
||||
status_transitions: {
|
||||
deactivated_at: 1719859200,
|
||||
},
|
||||
updated: 1719859200,
|
||||
value_settings: {
|
||||
event_payload_key: 'event_payload_key_2',
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformStripeMeterDataToMeterRepositoryData(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripeMeterId: 'met_1234',
|
||||
displayName: 'Meter 2',
|
||||
eventName: 'event_2',
|
||||
status: BillingMeterStatus.INACTIVE,
|
||||
customerMapping: {
|
||||
event_payload_key: 'event_payload_key_2',
|
||||
type: 'by_id',
|
||||
},
|
||||
eventTimeWindow: undefined,
|
||||
valueSettings: {
|
||||
event_payload_key: 'event_payload_key_2',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,219 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
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';
|
||||
import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util';
|
||||
describe('transformStripePriceDataToPriceRepositoryData', () => {
|
||||
const createMockPrice = (overrides = {}): Stripe.Price =>
|
||||
({
|
||||
id: 'price_123',
|
||||
active: true,
|
||||
product: 'prod_123',
|
||||
currency: 'usd',
|
||||
nickname: null,
|
||||
tax_behavior: null,
|
||||
type: 'recurring',
|
||||
billing_scheme: 'per_unit',
|
||||
unit_amount_decimal: '1000',
|
||||
unit_amount: 1000,
|
||||
transform_quantity: null,
|
||||
recurring: {
|
||||
usage_type: 'licensed',
|
||||
interval: 'month',
|
||||
meter: null,
|
||||
},
|
||||
currency_options: null,
|
||||
tiers: null,
|
||||
tiers_mode: null,
|
||||
...overrides,
|
||||
}) as unknown as Stripe.Price;
|
||||
|
||||
it('should transform basic price data correctly', () => {
|
||||
const mockPrice = createMockPrice();
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripePriceId: 'price_123',
|
||||
active: true,
|
||||
stripeProductId: 'prod_123',
|
||||
stripeMeterId: null,
|
||||
currency: 'USD',
|
||||
nickname: undefined,
|
||||
taxBehavior: undefined,
|
||||
type: BillingPriceType.RECURRING,
|
||||
billingScheme: BillingPriceBillingScheme.PER_UNIT,
|
||||
unitAmountDecimal: '1000',
|
||||
unitAmount: 1000,
|
||||
transformQuantity: undefined,
|
||||
usageType: BillingUsageType.LICENSED,
|
||||
interval: SubscriptionInterval.Month,
|
||||
currencyOptions: undefined,
|
||||
tiers: undefined,
|
||||
tiersMode: undefined,
|
||||
recurring: {
|
||||
usage_type: 'licensed',
|
||||
interval: 'month',
|
||||
meter: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('tax behavior transformations', () => {
|
||||
it.each([
|
||||
['exclusive', BillingPriceTaxBehavior.EXCLUSIVE],
|
||||
['inclusive', BillingPriceTaxBehavior.INCLUSIVE],
|
||||
['unspecified', BillingPriceTaxBehavior.UNSPECIFIED],
|
||||
])(
|
||||
'should transform tax behavior %s correctly',
|
||||
(stripeTaxBehavior, expected) => {
|
||||
const mockPrice = createMockPrice({
|
||||
tax_behavior: stripeTaxBehavior as Stripe.Price.TaxBehavior,
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.taxBehavior).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('price type transformations', () => {
|
||||
it.each([
|
||||
['one_time', BillingPriceType.ONE_TIME],
|
||||
['recurring', BillingPriceType.RECURRING],
|
||||
])('should transform price type %s correctly', (stripeType, expected) => {
|
||||
const mockPrice = createMockPrice({
|
||||
type: stripeType as Stripe.Price.Type,
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.type).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('billing scheme transformations', () => {
|
||||
it.each([
|
||||
['per_unit', BillingPriceBillingScheme.PER_UNIT],
|
||||
['tiered', BillingPriceBillingScheme.TIERED],
|
||||
])(
|
||||
'should transform billing scheme %s correctly',
|
||||
(stripeScheme, expected) => {
|
||||
const mockPrice = createMockPrice({
|
||||
billing_scheme: stripeScheme as Stripe.Price.BillingScheme,
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.billingScheme).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('recurring price configurations', () => {
|
||||
it('should handle metered pricing with meter ID', () => {
|
||||
const mockPrice = createMockPrice({
|
||||
recurring: {
|
||||
usage_type: 'metered',
|
||||
interval: 'month',
|
||||
meter: 'meter_123',
|
||||
},
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.stripeMeterId).toBe('meter_123');
|
||||
expect(result.usageType).toBe(BillingUsageType.METERED);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['month', SubscriptionInterval.Month],
|
||||
['day', SubscriptionInterval.Day],
|
||||
['week', SubscriptionInterval.Week],
|
||||
['year', SubscriptionInterval.Year],
|
||||
])('should transform interval %s correctly', (stripeInterval, expected) => {
|
||||
const mockPrice = createMockPrice({
|
||||
recurring: {
|
||||
usage_type: 'licensed',
|
||||
interval: stripeInterval as Stripe.Price.Recurring.Interval,
|
||||
meter: null,
|
||||
},
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.interval).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tiered pricing configurations', () => {
|
||||
const mockTiers = [
|
||||
{ up_to: 10, unit_amount: 1000 },
|
||||
{ up_to: 20, unit_amount: 800 },
|
||||
];
|
||||
|
||||
it.each([
|
||||
['graduated', BillingPriceTiersMode.GRADUATED],
|
||||
['volume', BillingPriceTiersMode.VOLUME],
|
||||
])(
|
||||
'should transform tiers mode %s correctly',
|
||||
(stripeTiersMode, expected) => {
|
||||
const mockPrice = createMockPrice({
|
||||
billing_scheme: 'tiered',
|
||||
tiers: mockTiers,
|
||||
tiers_mode: stripeTiersMode as Stripe.Price.TiersMode,
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.tiersMode).toBe(expected);
|
||||
expect(result.tiers).toEqual(mockTiers);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('optional fields handling', () => {
|
||||
it('should handle transform quantity configuration', () => {
|
||||
const transformQuantity = {
|
||||
divide_by: 100,
|
||||
round: 'up',
|
||||
};
|
||||
const mockPrice = createMockPrice({
|
||||
transform_quantity: transformQuantity,
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.transformQuantity).toEqual(transformQuantity);
|
||||
});
|
||||
|
||||
it('should handle currency options', () => {
|
||||
const currencyOptions = {
|
||||
eur: {
|
||||
unit_amount: 850,
|
||||
unit_amount_decimal: '850',
|
||||
},
|
||||
};
|
||||
const mockPrice = createMockPrice({ currency_options: currencyOptions });
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.currencyOptions).toEqual(currencyOptions);
|
||||
});
|
||||
|
||||
it('should handle null and undefined fields correctly', () => {
|
||||
const mockPrice = createMockPrice({
|
||||
nickname: null,
|
||||
unit_amount: null,
|
||||
unit_amount_decimal: null,
|
||||
transform_quantity: null,
|
||||
tiers: null,
|
||||
currency_options: null,
|
||||
});
|
||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
||||
|
||||
expect(result.nickname).toBeUndefined();
|
||||
expect(result.unitAmount).toBeUndefined();
|
||||
expect(result.unitAmountDecimal).toBeUndefined();
|
||||
expect(result.transformQuantity).toBeUndefined();
|
||||
expect(result.tiers).toBeUndefined();
|
||||
expect(result.currencyOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,234 @@
|
||||
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';
|
||||
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util';
|
||||
|
||||
describe('transformStripePriceEventToPriceRepositoryData', () => {
|
||||
const createMockPriceData = (overrides = {}) => ({
|
||||
object: {
|
||||
id: 'price_123',
|
||||
active: true,
|
||||
product: 'prod_123',
|
||||
meter: null,
|
||||
currency: 'usd',
|
||||
nickname: null,
|
||||
tax_behavior: null,
|
||||
type: 'recurring',
|
||||
billing_scheme: 'per_unit',
|
||||
unit_amount_decimal: '1000',
|
||||
unit_amount: 1000,
|
||||
transform_quantity: null,
|
||||
recurring: {
|
||||
usage_type: 'licensed',
|
||||
interval: 'month',
|
||||
},
|
||||
currency_options: null,
|
||||
tiers: null,
|
||||
tiers_mode: null,
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
it('should transform basic price data correctly', () => {
|
||||
const mockData = createMockPriceData();
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripePriceId: 'price_123',
|
||||
active: true,
|
||||
stripeProductId: 'prod_123',
|
||||
stripeMeterId: undefined,
|
||||
currency: 'USD',
|
||||
nickname: undefined,
|
||||
taxBehavior: undefined,
|
||||
type: BillingPriceType.RECURRING,
|
||||
billingScheme: BillingPriceBillingScheme.PER_UNIT,
|
||||
unitAmountDecimal: '1000',
|
||||
unitAmount: 1000,
|
||||
transformQuantity: undefined,
|
||||
usageType: BillingUsageType.LICENSED,
|
||||
interval: SubscriptionInterval.Month,
|
||||
currencyOptions: undefined,
|
||||
tiers: undefined,
|
||||
tiersMode: undefined,
|
||||
recurring: {
|
||||
usage_type: 'licensed',
|
||||
interval: 'month',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all tax behaviors correctly', () => {
|
||||
const taxBehaviors = [
|
||||
['exclusive', BillingPriceTaxBehavior.EXCLUSIVE],
|
||||
['inclusive', BillingPriceTaxBehavior.INCLUSIVE],
|
||||
['unspecified', BillingPriceTaxBehavior.UNSPECIFIED],
|
||||
];
|
||||
|
||||
taxBehaviors.forEach(([stripeTaxBehavior, expectedTaxBehavior]) => {
|
||||
const mockData = createMockPriceData({
|
||||
tax_behavior: stripeTaxBehavior,
|
||||
});
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.taxBehavior).toBe(expectedTaxBehavior);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all price types correctly', () => {
|
||||
const priceTypes = [
|
||||
['one_time', BillingPriceType.ONE_TIME],
|
||||
['recurring', BillingPriceType.RECURRING],
|
||||
];
|
||||
|
||||
priceTypes.forEach(([stripeType, expectedType]) => {
|
||||
const mockData = createMockPriceData({ type: stripeType });
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.type).toBe(expectedType);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all billing schemes correctly', () => {
|
||||
const billingSchemes = [
|
||||
['per_unit', BillingPriceBillingScheme.PER_UNIT],
|
||||
['tiered', BillingPriceBillingScheme.TIERED],
|
||||
];
|
||||
|
||||
billingSchemes.forEach(([stripeScheme, expectedScheme]) => {
|
||||
const mockData = createMockPriceData({ billing_scheme: stripeScheme });
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.billingScheme).toBe(expectedScheme);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all usage types correctly', () => {
|
||||
const usageTypes = [
|
||||
['licensed', BillingUsageType.LICENSED],
|
||||
['metered', BillingUsageType.METERED],
|
||||
];
|
||||
|
||||
usageTypes.forEach(([stripeUsageType, expectedUsageType]) => {
|
||||
const mockData = createMockPriceData({
|
||||
recurring: { usage_type: stripeUsageType, interval: 'month' },
|
||||
});
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.usageType).toBe(expectedUsageType);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all tiers modes correctly', () => {
|
||||
const tiersModes = [
|
||||
['graduated', BillingPriceTiersMode.GRADUATED],
|
||||
['volume', BillingPriceTiersMode.VOLUME],
|
||||
];
|
||||
|
||||
tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => {
|
||||
const mockData = createMockPriceData({ tiers_mode: stripeTiersMode });
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.tiersMode).toBe(expectedTiersMode);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all intervals correctly', () => {
|
||||
const intervals = [
|
||||
['month', SubscriptionInterval.Month],
|
||||
['day', SubscriptionInterval.Day],
|
||||
['week', SubscriptionInterval.Week],
|
||||
['year', SubscriptionInterval.Year],
|
||||
];
|
||||
|
||||
intervals.forEach(([stripeInterval, expectedInterval]) => {
|
||||
const mockData = createMockPriceData({
|
||||
recurring: { usage_type: 'licensed', interval: stripeInterval },
|
||||
});
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.interval).toBe(expectedInterval);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle tiered pricing configuration', () => {
|
||||
const mockTiers = [
|
||||
{ up_to: 10, unit_amount: 1000 },
|
||||
{ up_to: 20, unit_amount: 800 },
|
||||
];
|
||||
|
||||
const mockData = createMockPriceData({
|
||||
billing_scheme: 'tiered',
|
||||
tiers: mockTiers,
|
||||
tiers_mode: 'graduated',
|
||||
});
|
||||
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED);
|
||||
expect(result.tiers).toEqual(mockTiers);
|
||||
expect(result.tiersMode).toBe(BillingPriceTiersMode.GRADUATED);
|
||||
});
|
||||
|
||||
it('should handle metered pricing with transform quantity', () => {
|
||||
const mockTransformQuantity = {
|
||||
divide_by: 100,
|
||||
round: 'up',
|
||||
};
|
||||
|
||||
const mockData = createMockPriceData({
|
||||
recurring: {
|
||||
usage_type: 'metered',
|
||||
interval: 'month',
|
||||
meter: 'meter_123',
|
||||
},
|
||||
transform_quantity: mockTransformQuantity,
|
||||
});
|
||||
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.stripeMeterId).toBe('meter_123');
|
||||
expect(result.usageType).toBe(BillingUsageType.METERED);
|
||||
expect(result.transformQuantity).toEqual(mockTransformQuantity);
|
||||
});
|
||||
|
||||
it('should handle currency options', () => {
|
||||
const mockCurrencyOptions = {
|
||||
eur: {
|
||||
unit_amount: 850,
|
||||
unit_amount_decimal: '850',
|
||||
},
|
||||
};
|
||||
|
||||
const mockData = createMockPriceData({
|
||||
currency_options: mockCurrencyOptions,
|
||||
});
|
||||
|
||||
const result = transformStripePriceEventToPriceRepositoryData(
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.currencyOptions).toEqual(mockCurrencyOptions);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,86 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util';
|
||||
describe('transformStripeProductDataToProductRepositoryData', () => {
|
||||
it('should return the correct data', () => {
|
||||
const data: Stripe.Product = {
|
||||
id: 'prod_123',
|
||||
name: 'Product 1',
|
||||
active: true,
|
||||
description: 'Description 1',
|
||||
images: ['image1.jpg', 'image2.jpg'],
|
||||
marketing_features: [
|
||||
{
|
||||
name: 'feature1',
|
||||
},
|
||||
],
|
||||
created: 1719859200,
|
||||
updated: 1719859200,
|
||||
type: 'service',
|
||||
livemode: false,
|
||||
package_dimensions: null,
|
||||
shippable: false,
|
||||
object: 'product',
|
||||
default_price: 'price_123',
|
||||
unit_label: 'Unit',
|
||||
url: 'https://example.com',
|
||||
tax_code: 'tax_code_1',
|
||||
metadata: { key: 'value' },
|
||||
};
|
||||
|
||||
const result = transformStripeProductDataToProductRepositoryData(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripeProductId: 'prod_123',
|
||||
name: 'Product 1',
|
||||
active: true,
|
||||
description: 'Description 1',
|
||||
images: ['image1.jpg', 'image2.jpg'],
|
||||
marketingFeatures: [{ name: 'feature1' }],
|
||||
defaultStripePriceId: 'price_123',
|
||||
unitLabel: 'Unit',
|
||||
url: 'https://example.com',
|
||||
taxCode: 'tax_code_1',
|
||||
metadata: { key: 'value' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct data with null values', () => {
|
||||
const data: Stripe.Product = {
|
||||
id: 'prod_456',
|
||||
name: 'Product 2',
|
||||
active: false,
|
||||
description: '',
|
||||
images: [],
|
||||
created: 1719859200,
|
||||
updated: 1719859200,
|
||||
type: 'service',
|
||||
livemode: false,
|
||||
package_dimensions: null,
|
||||
shippable: false,
|
||||
object: 'product',
|
||||
marketing_features: [],
|
||||
default_price: null,
|
||||
unit_label: null,
|
||||
url: null,
|
||||
tax_code: null,
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
const result = transformStripeProductDataToProductRepositoryData(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripeProductId: 'prod_456',
|
||||
name: 'Product 2',
|
||||
active: false,
|
||||
description: '',
|
||||
images: [],
|
||||
marketingFeatures: [],
|
||||
defaultStripePriceId: undefined,
|
||||
unitLabel: undefined,
|
||||
url: undefined,
|
||||
taxCode: undefined,
|
||||
metadata: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,89 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { transformStripeProductEventToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util';
|
||||
|
||||
describe('transformStripeProductEventToProductRepositoryData', () => {
|
||||
it('should return the correct data', () => {
|
||||
const data: Stripe.ProductCreatedEvent.Data = {
|
||||
object: {
|
||||
id: 'prod_123',
|
||||
name: 'Product 1',
|
||||
active: true,
|
||||
description: 'Description 1',
|
||||
images: ['image1.jpg', 'image2.jpg'],
|
||||
marketing_features: [
|
||||
{
|
||||
name: 'feature1',
|
||||
},
|
||||
],
|
||||
created: 1719859200,
|
||||
updated: 1719859200,
|
||||
type: 'service',
|
||||
livemode: false,
|
||||
package_dimensions: null,
|
||||
shippable: false,
|
||||
object: 'product',
|
||||
default_price: 'price_123',
|
||||
unit_label: 'Unit',
|
||||
url: 'https://example.com',
|
||||
tax_code: 'tax_code_1',
|
||||
metadata: { key: 'value' },
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformStripeProductEventToProductRepositoryData(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripeProductId: 'prod_123',
|
||||
name: 'Product 1',
|
||||
active: true,
|
||||
description: 'Description 1',
|
||||
images: ['image1.jpg', 'image2.jpg'],
|
||||
marketingFeatures: [{ name: 'feature1' }],
|
||||
defaultStripePriceId: 'price_123',
|
||||
unitLabel: 'Unit',
|
||||
url: 'https://example.com',
|
||||
taxCode: 'tax_code_1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct data with null values', () => {
|
||||
const data: Stripe.ProductUpdatedEvent.Data = {
|
||||
object: {
|
||||
id: 'prod_456',
|
||||
name: 'Product 2',
|
||||
object: 'product',
|
||||
active: false,
|
||||
description: '',
|
||||
images: [],
|
||||
created: 1719859200,
|
||||
updated: 1719859200,
|
||||
type: 'service',
|
||||
livemode: false,
|
||||
package_dimensions: null,
|
||||
shippable: false,
|
||||
marketing_features: [],
|
||||
default_price: null,
|
||||
unit_label: null,
|
||||
url: null,
|
||||
tax_code: null,
|
||||
metadata: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformStripeProductEventToProductRepositoryData(data);
|
||||
|
||||
expect(result).toEqual({
|
||||
stripeProductId: 'prod_456',
|
||||
name: 'Product 2',
|
||||
active: false,
|
||||
description: '',
|
||||
images: [],
|
||||
marketingFeatures: [],
|
||||
defaultStripePriceId: undefined,
|
||||
unitLabel: undefined,
|
||||
url: undefined,
|
||||
taxCode: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,85 @@
|
||||
import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util';
|
||||
|
||||
describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
|
||||
const mockWorkspaceId = 'workspace_123';
|
||||
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
|
||||
|
||||
const createMockSubscriptionData = (overrides = {}) => ({
|
||||
object: {
|
||||
id: 'sub_123',
|
||||
customer: 'cus_123',
|
||||
status: 'active',
|
||||
items: {
|
||||
data: [
|
||||
{
|
||||
plan: {
|
||||
interval: 'month',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
cancel_at_period_end: false,
|
||||
currency: 'usd',
|
||||
current_period_end: mockTimestamp,
|
||||
current_period_start: mockTimestamp - 2592000, // 30 days before end
|
||||
metadata: {},
|
||||
collection_method: 'charge_automatically',
|
||||
automatic_tax: null,
|
||||
cancellation_details: null,
|
||||
ended_at: null,
|
||||
trial_start: null,
|
||||
trial_end: null,
|
||||
cancel_at: null,
|
||||
canceled_at: null,
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
it('should transform basic customer data correctly', () => {
|
||||
const mockData = createMockSubscriptionData('cus_123');
|
||||
|
||||
const result = transformStripeSubscriptionEventToCustomerRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
workspaceId: 'workspace_123',
|
||||
stripeCustomerId: 'cus_123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with different subscription event types', () => {
|
||||
const mockData = createMockSubscriptionData('cus_123');
|
||||
|
||||
// Test with different event types (they should all transform the same way)
|
||||
['updated', 'created', 'deleted'].forEach(() => {
|
||||
const result = transformStripeSubscriptionEventToCustomerRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
workspaceId: 'workspace_123',
|
||||
stripeCustomerId: 'cus_123',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different workspace IDs', () => {
|
||||
const mockData = createMockSubscriptionData('cus_123');
|
||||
const testWorkspaces = ['workspace_1', 'workspace_2', 'workspace_abc'];
|
||||
|
||||
testWorkspaces.forEach((testWorkspaceId) => {
|
||||
const result = transformStripeSubscriptionEventToCustomerRepositoryData(
|
||||
testWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
workspaceId: testWorkspaceId,
|
||||
stripeCustomerId: 'cus_123',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,197 @@
|
||||
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 { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util';
|
||||
|
||||
describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
||||
const mockWorkspaceId = 'workspace-123';
|
||||
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
|
||||
|
||||
const createMockSubscriptionData = (overrides = {}) => ({
|
||||
object: {
|
||||
id: 'sub_123',
|
||||
customer: 'cus_123',
|
||||
status: 'active',
|
||||
items: {
|
||||
data: [
|
||||
{
|
||||
plan: {
|
||||
interval: 'month',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
cancel_at_period_end: false,
|
||||
currency: 'usd',
|
||||
current_period_end: mockTimestamp,
|
||||
current_period_start: mockTimestamp - 2592000, // 30 days before end
|
||||
metadata: {},
|
||||
collection_method: 'charge_automatically',
|
||||
automatic_tax: null,
|
||||
cancellation_details: null,
|
||||
ended_at: null,
|
||||
trial_start: null,
|
||||
trial_end: null,
|
||||
cancel_at: null,
|
||||
canceled_at: null,
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
||||
it('should transform basic subscription data correctly', () => {
|
||||
const mockData = createMockSubscriptionData();
|
||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
workspaceId: mockWorkspaceId,
|
||||
stripeCustomerId: 'cus_123',
|
||||
stripeSubscriptionId: 'sub_123',
|
||||
status: SubscriptionStatus.Active,
|
||||
interval: 'month',
|
||||
cancelAtPeriodEnd: false,
|
||||
currency: 'USD',
|
||||
currentPeriodEnd: new Date(mockTimestamp * 1000),
|
||||
currentPeriodStart: new Date((mockTimestamp - 2592000) * 1000),
|
||||
metadata: {},
|
||||
collectionMethod:
|
||||
BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY,
|
||||
automaticTax: undefined,
|
||||
cancellationDetails: undefined,
|
||||
endedAt: undefined,
|
||||
trialStart: undefined,
|
||||
trialEnd: undefined,
|
||||
cancelAt: undefined,
|
||||
canceledAt: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle all subscription statuses correctly', () => {
|
||||
const statuses = [
|
||||
['active', SubscriptionStatus.Active],
|
||||
['canceled', SubscriptionStatus.Canceled],
|
||||
['incomplete', SubscriptionStatus.Incomplete],
|
||||
['incomplete_expired', SubscriptionStatus.IncompleteExpired],
|
||||
['past_due', SubscriptionStatus.PastDue],
|
||||
['paused', SubscriptionStatus.Paused],
|
||||
['trialing', SubscriptionStatus.Trialing],
|
||||
['unpaid', SubscriptionStatus.Unpaid],
|
||||
];
|
||||
|
||||
statuses.forEach(([stripeStatus, expectedStatus]) => {
|
||||
const mockData = createMockSubscriptionData({
|
||||
status: stripeStatus,
|
||||
});
|
||||
const result =
|
||||
transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.status).toBe(expectedStatus);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle subscription with trial periods', () => {
|
||||
const trialStart = mockTimestamp - 604800; // 7 days before
|
||||
const trialEnd = mockTimestamp + 604800; // 7 days after
|
||||
|
||||
const mockData = createMockSubscriptionData({
|
||||
trial_start: trialStart,
|
||||
trial_end: trialEnd,
|
||||
});
|
||||
|
||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.trialStart).toEqual(new Date(trialStart * 1000));
|
||||
expect(result.trialEnd).toEqual(new Date(trialEnd * 1000));
|
||||
});
|
||||
|
||||
it('should handle subscription cancellation details', () => {
|
||||
const cancelAt = mockTimestamp + 2592000; // 30 days after
|
||||
const canceledAt = mockTimestamp;
|
||||
const mockData = createMockSubscriptionData({
|
||||
cancel_at: cancelAt,
|
||||
canceled_at: canceledAt,
|
||||
cancel_at_period_end: true,
|
||||
cancellation_details: {
|
||||
comment: 'Customer requested cancellation',
|
||||
feedback: 'too_expensive',
|
||||
reason: 'customer_request',
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.cancelAt).toEqual(new Date(cancelAt * 1000));
|
||||
expect(result.canceledAt).toEqual(new Date(canceledAt * 1000));
|
||||
expect(result.cancelAtPeriodEnd).toBe(true);
|
||||
expect(result.cancellationDetails).toEqual({
|
||||
comment: 'Customer requested cancellation',
|
||||
feedback: 'too_expensive',
|
||||
reason: 'customer_request',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle automatic tax information', () => {
|
||||
const mockData = createMockSubscriptionData({
|
||||
automatic_tax: {
|
||||
enabled: true,
|
||||
status: 'calculated',
|
||||
},
|
||||
});
|
||||
|
||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.automaticTax).toEqual({
|
||||
enabled: true,
|
||||
status: 'calculated',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different collection methods', () => {
|
||||
const methods = [
|
||||
[
|
||||
'charge_automatically',
|
||||
BillingSubscriptionCollectionMethod.CHARGE_AUTOMATICALLY,
|
||||
],
|
||||
['send_invoice', BillingSubscriptionCollectionMethod.SEND_INVOICE],
|
||||
];
|
||||
|
||||
methods.forEach(([stripeMethod, expectedMethod]) => {
|
||||
const mockData = createMockSubscriptionData({
|
||||
collection_method: stripeMethod,
|
||||
});
|
||||
const result =
|
||||
transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.collectionMethod).toBe(expectedMethod);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different currencies', () => {
|
||||
const mockData = createMockSubscriptionData({
|
||||
currency: 'eur',
|
||||
});
|
||||
|
||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
||||
mockWorkspaceId,
|
||||
mockData as any,
|
||||
);
|
||||
|
||||
expect(result.currency).toBe('EUR');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,112 @@
|
||||
import request from 'supertest';
|
||||
import { createMockStripeEntitlementUpdatedData } from 'test/integration/billing/utils/create-mock-stripe-entitlement-updated-data.util';
|
||||
import { createMockStripePriceCreatedData } from 'test/integration/billing/utils/create-mock-stripe-price-created-data.util';
|
||||
import { createMockStripeProductUpdatedData } from 'test/integration/billing/utils/create-mock-stripe-product-updated-data.util';
|
||||
import { createMockStripeSubscriptionCreatedData } from 'test/integration/billing/utils/create-mock-stripe-subscription-created-data.util';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
describe('BillingController (integration)', () => {
|
||||
it('should handle product.updated and price.created webhook events', async () => {
|
||||
const productUpdatedPayload = {
|
||||
type: 'product.updated',
|
||||
data: createMockStripeProductUpdatedData(),
|
||||
};
|
||||
const priceCreatedPayload = {
|
||||
type: 'price.created',
|
||||
data: createMockStripePriceCreatedData(),
|
||||
};
|
||||
|
||||
await client
|
||||
.post('/billing/webhooks')
|
||||
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
|
||||
.set('stripe-signature', 'correct-signature')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify(productUpdatedPayload))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.stripeProductId).toBeDefined();
|
||||
});
|
||||
|
||||
await client
|
||||
.post('/billing/webhooks')
|
||||
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
|
||||
.set('stripe-signature', 'correct-signature')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify(priceCreatedPayload))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.stripePriceId).toBeDefined();
|
||||
expect(res.body.stripeMeterId).toBeDefined();
|
||||
});
|
||||
});
|
||||
it('should handle subscription.created webhook event', async () => {
|
||||
const subscriptionCreatedPayload = {
|
||||
type: 'customer.subscription.created',
|
||||
data: createMockStripeSubscriptionCreatedData(),
|
||||
};
|
||||
const entitlementUpdatedPayload = {
|
||||
type: 'entitlements.active_entitlement_summary.updated',
|
||||
data: createMockStripeEntitlementUpdatedData(),
|
||||
};
|
||||
|
||||
await client
|
||||
.post('/billing/webhooks')
|
||||
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
|
||||
.set('stripe-signature', 'correct-signature')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify(subscriptionCreatedPayload))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.stripeSubscriptionId).toBeDefined();
|
||||
expect(res.body.stripeCustomerId).toBeDefined();
|
||||
});
|
||||
|
||||
await client
|
||||
.post('/billing/webhooks')
|
||||
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
|
||||
.set('stripe-signature', 'correct-signature')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify(entitlementUpdatedPayload))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
expect(res.body.stripeEntitlementCustomerId).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle entitlements.active_entitlement_summary.updated when the subscription is not found', async () => {
|
||||
const entitlementUpdatedPayload = {
|
||||
type: 'entitlements.active_entitlement_summary.updated',
|
||||
data: createMockStripeEntitlementUpdatedData({
|
||||
customer: 'new_customer',
|
||||
}),
|
||||
};
|
||||
|
||||
await client
|
||||
.post('/billing/webhooks')
|
||||
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
|
||||
.set('stripe-signature', 'correct-signature')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify(entitlementUpdatedPayload))
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should reject webhook with invalid signature', async () => {
|
||||
const entitlementUpdatedPayload = {
|
||||
type: 'customer.entitlement.created',
|
||||
data: {
|
||||
object: {
|
||||
id: 'ent_test123',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await client
|
||||
.post('/billing/webhooks')
|
||||
.set('Authorization', `Bearer ${ACCESS_TOKEN}`)
|
||||
.set('stripe-signature', 'invalid-signature')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(JSON.stringify(entitlementUpdatedPayload))
|
||||
.expect(500);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import Stripe from 'stripe';
|
||||
export const createMockStripeEntitlementUpdatedData = (
|
||||
overrides = {},
|
||||
): Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data => ({
|
||||
object: {
|
||||
object: 'entitlements.active_entitlement_summary',
|
||||
customer: 'cus_default1',
|
||||
livemode: false,
|
||||
entitlements: {
|
||||
object: 'list',
|
||||
data: [
|
||||
{
|
||||
id: 'ent_test_61',
|
||||
object: 'entitlements.active_entitlement',
|
||||
feature: 'feat_test_61',
|
||||
livemode: false,
|
||||
lookup_key: 'SSO',
|
||||
},
|
||||
],
|
||||
has_more: false,
|
||||
url: '/v1/customer/cus_Q/entitlements',
|
||||
},
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
import Stripe from 'stripe';
|
||||
export const createMockStripePriceCreatedData = (
|
||||
overrides = {},
|
||||
): Stripe.PriceCreatedEvent.Data => ({
|
||||
object: {
|
||||
id: 'price_1Q',
|
||||
object: 'price',
|
||||
active: true,
|
||||
billing_scheme: 'per_unit',
|
||||
created: 1733734326,
|
||||
currency: 'usd',
|
||||
custom_unit_amount: null,
|
||||
livemode: false,
|
||||
lookup_key: null,
|
||||
metadata: {},
|
||||
nickname: null,
|
||||
product: 'prod_RLN',
|
||||
recurring: {
|
||||
aggregate_usage: null,
|
||||
interval: 'month',
|
||||
interval_count: 1,
|
||||
meter: null,
|
||||
trial_period_days: null,
|
||||
usage_type: 'licensed',
|
||||
},
|
||||
tax_behavior: 'unspecified',
|
||||
tiers_mode: null,
|
||||
transform_quantity: null,
|
||||
type: 'recurring',
|
||||
unit_amount: 0,
|
||||
unit_amount_decimal: '0',
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export const createMockStripeProductUpdatedData = (
|
||||
overrides = {},
|
||||
): Stripe.ProductUpdatedEvent.Data => ({
|
||||
object: {
|
||||
id: 'prod_RLN',
|
||||
object: 'product',
|
||||
active: true,
|
||||
created: 1733410584,
|
||||
default_price: null,
|
||||
description: null,
|
||||
images: [],
|
||||
livemode: false,
|
||||
marketing_features: [],
|
||||
metadata: {},
|
||||
name: 'kjnnjkjknkjnjkn',
|
||||
package_dimensions: null,
|
||||
shippable: null,
|
||||
statement_descriptor: null,
|
||||
tax_code: 'txcd_10103001',
|
||||
type: 'service',
|
||||
unit_label: null,
|
||||
updated: 1734694649,
|
||||
url: null,
|
||||
},
|
||||
previous_attributes: {
|
||||
default_price: 'price_1Q',
|
||||
updated: 1733410585,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
@ -0,0 +1,130 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export const createMockStripeSubscriptionCreatedData = (
|
||||
overrides = {},
|
||||
): Stripe.CustomerSubscriptionCreatedEvent.Data => ({
|
||||
object: {
|
||||
object: 'subscription',
|
||||
id: 'sub_default',
|
||||
customer: 'cus_default1',
|
||||
status: 'active',
|
||||
items: {
|
||||
data: [
|
||||
{
|
||||
plan: {
|
||||
id: 'plan_default',
|
||||
object: 'plan',
|
||||
active: true,
|
||||
aggregate_usage: null,
|
||||
amount_decimal: '0',
|
||||
billing_scheme: 'per_unit',
|
||||
interval_count: 1,
|
||||
livemode: false,
|
||||
nickname: null,
|
||||
tiers_mode: null,
|
||||
transform_usage: null,
|
||||
trial_period_days: null,
|
||||
interval: 'month',
|
||||
currency: 'usd',
|
||||
amount: 0,
|
||||
created: 1672531200,
|
||||
product: 'prod_default',
|
||||
usage_type: 'licensed',
|
||||
metadata: {},
|
||||
meter: null,
|
||||
},
|
||||
id: '',
|
||||
object: 'subscription_item',
|
||||
billing_thresholds: null,
|
||||
created: 0,
|
||||
discounts: [],
|
||||
metadata: {},
|
||||
price: {
|
||||
id: 'price_default',
|
||||
object: 'price',
|
||||
active: true,
|
||||
billing_scheme: 'per_unit',
|
||||
created: 1672531200,
|
||||
currency: 'usd',
|
||||
custom_unit_amount: null,
|
||||
livemode: false,
|
||||
lookup_key: null,
|
||||
metadata: {},
|
||||
nickname: null,
|
||||
product: 'prod_default',
|
||||
recurring: {
|
||||
aggregate_usage: null,
|
||||
interval: 'month',
|
||||
interval_count: 1,
|
||||
meter: null,
|
||||
trial_period_days: null,
|
||||
usage_type: 'licensed',
|
||||
},
|
||||
tax_behavior: null,
|
||||
tiers_mode: null,
|
||||
transform_quantity: null,
|
||||
type: 'recurring',
|
||||
unit_amount: 1000,
|
||||
unit_amount_decimal: '1000',
|
||||
},
|
||||
subscription: '',
|
||||
tax_rates: null,
|
||||
},
|
||||
],
|
||||
object: 'list',
|
||||
has_more: false,
|
||||
url: '',
|
||||
},
|
||||
cancel_at_period_end: false,
|
||||
currency: 'usd',
|
||||
current_period_end: 1672531200,
|
||||
current_period_start: 1672531200,
|
||||
metadata: { workspaceId: '3b8e6458-5fc1-4e63-8563-008ccddaa6db' },
|
||||
trial_end: null,
|
||||
trial_start: null,
|
||||
canceled_at: null,
|
||||
...overrides,
|
||||
application: null,
|
||||
application_fee_percent: null,
|
||||
automatic_tax: {
|
||||
enabled: true,
|
||||
liability: {
|
||||
type: 'self',
|
||||
},
|
||||
},
|
||||
billing_cycle_anchor: 0,
|
||||
billing_cycle_anchor_config: null,
|
||||
billing_thresholds: null,
|
||||
cancel_at: null,
|
||||
cancellation_details: null,
|
||||
collection_method: 'charge_automatically',
|
||||
created: 0,
|
||||
days_until_due: null,
|
||||
default_payment_method: null,
|
||||
default_source: null,
|
||||
description: null,
|
||||
discount: null,
|
||||
discounts: [],
|
||||
ended_at: null,
|
||||
invoice_settings: {
|
||||
account_tax_ids: null,
|
||||
issuer: {
|
||||
type: 'self',
|
||||
},
|
||||
},
|
||||
latest_invoice: null,
|
||||
livemode: false,
|
||||
next_pending_invoice_item_invoice: null,
|
||||
on_behalf_of: null,
|
||||
pause_collection: null,
|
||||
payment_settings: null,
|
||||
pending_invoice_item_interval: null,
|
||||
pending_setup_intent: null,
|
||||
pending_update: null,
|
||||
schedule: null,
|
||||
start_date: 0,
|
||||
test_clock: null,
|
||||
transfer_data: null,
|
||||
trial_settings: null,
|
||||
},
|
||||
});
|
||||
@ -2,6 +2,8 @@ import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { Test, TestingModule, TestingModuleBuilder } from '@nestjs/testing';
|
||||
|
||||
import { AppModule } from 'src/app.module';
|
||||
import { StripeSDKMockService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/mocks/stripe-sdk-mock.service';
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
|
||||
interface TestingModuleCreatePreHook {
|
||||
(moduleBuilder: TestingModuleBuilder): TestingModuleBuilder;
|
||||
@ -23,9 +25,12 @@ export const createApp = async (
|
||||
appInitHook?: TestingAppCreatePreHook;
|
||||
} = {},
|
||||
): Promise<NestExpressApplication> => {
|
||||
const stripeSDKMockService = new StripeSDKMockService();
|
||||
let moduleBuilder: TestingModuleBuilder = Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
});
|
||||
})
|
||||
.overrideProvider(StripeSDKService)
|
||||
.useValue(stripeSDKMockService);
|
||||
|
||||
if (config.moduleBuilderHook) {
|
||||
moduleBuilder = config.moduleBuilderHook(moduleBuilder);
|
||||
@ -33,7 +38,10 @@ export const createApp = async (
|
||||
|
||||
const moduleFixture: TestingModule = await moduleBuilder.compile();
|
||||
|
||||
const app = moduleFixture.createNestApplication<NestExpressApplication>();
|
||||
const app = moduleFixture.createNestApplication<NestExpressApplication>({
|
||||
rawBody: true,
|
||||
cors: true,
|
||||
});
|
||||
|
||||
if (config.appInitHook) {
|
||||
await config.appInitHook(app);
|
||||
|
||||
Reference in New Issue
Block a user