Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 79 additions & 5 deletions src/infrastructure/adapters/stripe.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import { Customer, CreateCustomerParams, UpdateCustomerParams } from '../domain/
import envVariablesConfig from '../../config';
import { PaymentMethod } from '../domain/entities/paymentMethod';

import { UserType } from '../../core/users/User';
import { Price, PriceInterval } from '../domain/entities/price';

export class StripePaymentsAdapter implements PaymentsAdapter {
private readonly provider: Stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, {
readonly provider: Stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, {
apiVersion: '2025-02-24.acacia',
});

getInstance(): Stripe {
return this.provider;
}

async createCustomer(params: Partial<CreateCustomerParams>): Promise<Customer> {
const stripeCustomer = await this.provider.customers.create(this.toStripeCustomerParams(params));

Expand Down Expand Up @@ -55,6 +54,56 @@ export class StripePaymentsAdapter implements PaymentsAdapter {
return PaymentMethod.toDomain(paymentMethods);
}

async getPrices(currency: string = 'eur'): Promise<Price[]> {
const prices = await this.provider.prices.search({
query: `metadata["show"]:"1" active:"true" currency:"${currency}"`,
expand: ['data.currency_options', 'data.product'],
limit: 100,
});

return prices.data.map((price) => {
const businessSeats = this.getBusinessSeats(price);

return Price.toDomain({
id: price.id,
productId: (price.product as Stripe.Product).id,
bytes: Number.parseInt(price.metadata.bytes),
interval: this.getInterval(price.recurring!.interval),
commitmentPlan: this.hasAnnualCommitment(price),
recurring: price.type === 'recurring',
amount: price.currency_options![currency].unit_amount as number,
currency: price.currency,
decimalAmount: (price.currency_options![currency].unit_amount as number) / 100,
type: price.metadata.type === 'business' ? UserType.Business : UserType.Individual,
...businessSeats,
});
});
}

async getPriceById(priceId: Price['id'], currency: string = 'eur'): Promise<Price> {
const price = await this.provider.prices.retrieve(priceId, {
expand: ['currency_options', 'product'],
});

const isBusinessPlan = price.metadata?.type === 'business';

const businessSeats = isBusinessPlan ? this.getBusinessSeats(price) : undefined;

return Price.toDomain({
id: price.id,
productId: (price.product as Stripe.Product).id,
bytes: Number.parseInt(price.metadata.bytes),
interval: this.getInterval(price.recurring?.interval),
commitmentPlan: this.hasAnnualCommitment(price),
recurring: price.type === 'recurring',
amount: price.currency_options![currency].unit_amount as number,
currency: price.currency,
decimalAmount: (price.currency_options![currency].unit_amount as number) / 100,
type: isBusinessPlan ? UserType.Business : UserType.Individual,
...businessSeats,
});
}

private toStripeCustomerParams(params: Partial<UpdateCustomerParams>): Stripe.CustomerCreateParams {
return {
...(params.name && { name: params.name }),
Expand All @@ -73,6 +122,31 @@ export class StripePaymentsAdapter implements PaymentsAdapter {
...(params.metadata && { metadata: params.metadata }),
};
}

private hasAnnualCommitment(price: Stripe.Price): boolean {
return price?.metadata.annualCommitment === 'true';
}

private getInterval(interval?: Stripe.Price.Recurring.Interval): PriceInterval {
switch (interval) {
case 'year':
return 'year';
case 'month':
return 'month';
default:
return 'lifetime';
}
}

private getBusinessSeats(price: Stripe.Price): {
minimumSeats: number | undefined;
maximumSeats: number | undefined;
} {
return {
minimumSeats: price.metadata.minimumSeats ? Number.parseInt(price.metadata.minimumSeats) : undefined,
maximumSeats: price.metadata.maximumSeats ? Number.parseInt(price.metadata.maximumSeats) : undefined,
};
}
}

export const stripePaymentsAdapter = new StripePaymentsAdapter();
64 changes: 64 additions & 0 deletions src/infrastructure/domain/entities/price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { UserType } from '../../../core/users/User';

export type PriceInterval = 'year' | 'month' | 'lifetime';

export interface PriceAttributes {
id: string;
productId: string;
bytes: number;
interval: PriceInterval;
commitmentPlan: boolean;
recurring: boolean;
amount: number;
currency: string;
decimalAmount: number;
type: UserType;
minimumSeats?: number;
maximumSeats?: number;
}

export class Price implements PriceAttributes {
id: string;
productId: string;
bytes: number;
interval: PriceInterval;
commitmentPlan: boolean;
recurring: boolean;
amount: number;
currency: string;
decimalAmount: number;
type: UserType;
minimumSeats?: number;
maximumSeats?: number;

private constructor(attributes: PriceAttributes) {
this.id = attributes.id;
this.productId = attributes.productId;
this.bytes = attributes.bytes;
this.interval = attributes.interval;
this.commitmentPlan = attributes.commitmentPlan;
this.amount = attributes.amount;
this.currency = attributes.currency;
this.decimalAmount = attributes.decimalAmount;
this.recurring = attributes.recurring;
this.type = attributes.type;
this.minimumSeats = attributes.minimumSeats;
this.maximumSeats = attributes.maximumSeats;
}

static toDomain(attributes: PriceAttributes): Price {
return new Price(attributes);
}

public isBusinessPlan(): boolean {
return this.type === UserType.Business;
}

public isCommitmentPlan(): boolean {
return this.commitmentPlan;
}

public isRecurring(): boolean {
return this.recurring;
}
}
18 changes: 18 additions & 0 deletions tests/src/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import jwt from 'jsonwebtoken';
import { Price, PriceAttributes } from '../../src/infrastructure/domain/entities/price';
import { randomUUID } from 'node:crypto';
import { FastifyBaseLogger } from 'fastify';
import { Chance } from 'chance';
Expand Down Expand Up @@ -218,6 +219,23 @@ export const getPromoCode = (params?: DeepPartial<Stripe.PromotionCode>): Stripe
};
};

export const getPriceEntity = (params?: Partial<PriceAttributes>): Price => {
const mockedStripePrice = getPrice();
return Price.toDomain({
id: mockedStripePrice.id,
productId: mockedStripePrice.product as string,
bytes: 1099511627776,
interval: 'year',
commitmentPlan: false,
recurring: true,
amount: mockedStripePrice.unit_amount as number,
currency: mockedStripePrice.currency,
decimalAmount: (mockedStripePrice.unit_amount as number) / 100,
type: UserType.Individual,
...params,
});
};

export const priceById = ({
bytes,
interval,
Expand Down
Loading
Loading