Skip to content
Closed
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
17 changes: 8 additions & 9 deletions src/controller/checkout.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getAllowedCurrencies, isValidCurrency } from '../utils/currency';
import { signUserToken } from '../utils/signUserToken';
import { verifyRecaptcha } from '../utils/verifyRecaptcha';
import { setupAuth } from '../plugins/auth';
import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter';
import { stripeAdapter } from '../infrastructure/adapters/stripe.adapter';
import { UserType } from '../core/users/User';

export function checkoutController(usersService: UsersService, paymentsService: PaymentService) {
Expand Down Expand Up @@ -85,7 +85,7 @@ export function checkoutController(usersService: UsersService, paymentsService:
const userExists = await usersService.findUserByUuid(userUuid).catch(() => null);

if (userExists) {
await stripePaymentsAdapter.updateCustomer(userExists.customerId, {
await stripeAdapter.updateCustomer(userExists.customerId, {
name: customerName,
email,
address: {
Expand All @@ -99,7 +99,7 @@ export function checkoutController(usersService: UsersService, paymentsService:
});
customerId = userExists.customerId;
} else {
const { id } = await stripePaymentsAdapter.createCustomer({
const { id } = await stripeAdapter.createCustomer({
name: customerName,
email,
address: {
Expand Down Expand Up @@ -190,7 +190,7 @@ export function checkoutController(usersService: UsersService, paymentsService:
throw new ForbiddenError();
}

const price = await paymentsService.getPriceById(priceId);
const price = await stripeAdapter.getPriceById(priceId);

if (price.type === UserType.Business) throw new BadRequestError('Business plan is no longer available');

Expand Down Expand Up @@ -286,7 +286,7 @@ export function checkoutController(usersService: UsersService, paymentsService:
throw new ForbiddenError();
}

const price = await paymentsService.getPriceById(priceId);
const price = await stripeAdapter.getPriceById(priceId);

if (price.interval !== 'lifetime') {
throw new BadRequestError('Only lifetime plans are supported');
Expand Down Expand Up @@ -362,12 +362,11 @@ export function checkoutController(usersService: UsersService, paymentsService:
const userUuid = req.user?.payload?.uuid;
const user = await usersService.findUserByUuid(userUuid).catch(() => null);

const price = await paymentsService.getPriceById(priceId, currency);

const price = await stripeAdapter.getPriceById(priceId, currency);
let amount = price.amount;

if (promoCodeName) {
const couponCode = await paymentsService.getPromoCodeByName(price.product, promoCodeName);
if (promoCodeName && price.productId) {
const couponCode = await paymentsService.getPromoCodeByName(price.productId, promoCodeName);
if (couponCode.amountOff) {
amount = Math.max(0, price.amount - couponCode.amountOff);
} else if (couponCode.percentOff) {
Expand Down
6 changes: 3 additions & 3 deletions src/controller/object-storage.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ForbiddenError, UnauthorizedError } from '../errors/Errors';
import config from '../config';
import Stripe from 'stripe';
import { setupAuth } from '../plugins/auth';
import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter';
import { stripeAdapter } from '../infrastructure/adapters/stripe.adapter';

function signUserToken(customerId: string) {
return jwt.sign({ customerId }, config.JWT_SECRET);
Expand Down Expand Up @@ -58,7 +58,7 @@ export function objectStorageController(paymentService: PaymentService) {
if (userExists) {
customerId = userExists.id;
} else {
const { id } = await stripePaymentsAdapter.createCustomer({
const { id } = await stripeAdapter.createCustomer({
name: customerName,
email,
address: {
Expand Down Expand Up @@ -186,7 +186,7 @@ export function objectStorageController(paymentService: PaymentService) {
const { planId, currency } = req.query;

try {
const planObject = await paymentService.getObjectStoragePlanById(planId, currency);
const planObject = await stripeAdapter.getPriceById(planId, currency);

return rep.status(200).send(planObject);
} catch (error) {
Expand Down
7 changes: 3 additions & 4 deletions src/controller/payments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { VERIFICATION_CHARGE } from '../constants';
import { setupAuth } from '../plugins/auth';
import { PaymentService } from '../services/payment.service';
import { InvalidLicenseCodeError } from '../errors/LicenseCodeErrors';
import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter';
import { stripeAdapter } from '../infrastructure/adapters/stripe.adapter';

const allowedCurrency = ['eur', 'usd'];

Expand Down Expand Up @@ -206,7 +206,7 @@ export function paymentsController(
async (req, rep) => {
const user = await assertUser(req, rep, usersService);
const { address, phoneNumber } = req.body;
await stripePaymentsAdapter.updateCustomer(user.customerId, {
await stripeAdapter.updateCustomer(user.customerId, {
address: {
line1: address,
},
Expand Down Expand Up @@ -425,15 +425,14 @@ export function paymentsController(
},
async (req, rep) => {
const { currency } = req.query;
const userType = (req.query.userType as UserType) || UserType.Individual;

const { currencyValue, isError, errorMessage } = checkCurrency(currency);

if (isError) {
return rep.status(400).send({ message: errorMessage });
}

return paymentService.getPrices(currencyValue, userType);
return stripeAdapter.getPrices(currencyValue);
},
);

Expand Down
4 changes: 2 additions & 2 deletions src/core/users/DetermineLifetimeConditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { User, UserType } from './User';
import { FREE_PLAN_BYTES_SPACE } from '../../constants';
import { BadRequestError } from '../../errors/Errors';
import Logger from '../../Logger';
import { stripePaymentsAdapter } from '../../infrastructure/adapters/stripe.adapter';
import { stripeAdapter } from '../../infrastructure/adapters/stripe.adapter';

export class DetermineLifetimeConditions {
constructor(
Expand Down Expand Up @@ -73,7 +73,7 @@ export class DetermineLifetimeConditions {
maxSpaceBytes: number;
tier: Tier;
}> {
const customer = await stripePaymentsAdapter.getCustomer(user.customerId);
const customer = await stripeAdapter.getCustomer(user.customerId);

const { email } = customer;
const customersRelatedToUser = await this.paymentsService.getCustomersByEmail(email);
Expand Down
3 changes: 3 additions & 0 deletions src/core/users/DisplayPrice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ export interface DisplayPrice {
interval: 'year' | 'month' | 'lifetime';
amount: number;
currency: string;
commitmentPlan?: boolean;
minimumSeats?: number;
maximumSeats?: number;
}
80 changes: 73 additions & 7 deletions src/infrastructure/adapters/stripe.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@ import { PaymentsAdapter } from '../domain/ports/payments.adapter';
import { Customer, CreateCustomerParams, UpdateCustomerParams } from '../domain/entities/customer';
import envVariablesConfig from '../../config';
import { PaymentMethod } from '../domain/entities/paymentMethod';
import { Price } from '../domain/entities/price';
import { UserType } from '../../core/users/User';

export class StripePaymentsAdapter implements PaymentsAdapter {
private readonly provider: Stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, {
export class StripeAdapter implements PaymentsAdapter {
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 +53,50 @@ 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) =>
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),
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,
}),
);
}

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),
amount: price.currency_options![currency].unit_amount as number,
currency: price.currency,
decimalAmount: (price.currency_options![currency].unit_amount as number) / 100,
...businessSeats,
});
}

private toStripeCustomerParams(params: Partial<UpdateCustomerParams>): Stripe.CustomerCreateParams {
return {
...(params.name && { name: params.name }),
Expand All @@ -73,6 +115,30 @@ 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): 'year' | 'month' | 'lifetime' {
switch (interval) {
case 'year':
return 'year';
case 'month':
return 'month';
default:
return 'lifetime';
}
}

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

export const stripePaymentsAdapter = new StripePaymentsAdapter();
export const stripeAdapter = new StripeAdapter();
52 changes: 52 additions & 0 deletions src/infrastructure/domain/entities/price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { RequestedPlanData } from '../../../types/subscription';

export class Price {
constructor(public readonly price: RequestedPlanData) {}

static toDomain(price: RequestedPlanData) {
return new Price(price);
}

get id() {
return this.price.id;
}

get productId() {
return this.price.productId;
}

get bytes() {
return this.price.bytes;
}

get amount() {
return this.price.amount;
}

get currency() {
return this.price.currency;
}

get interval() {
return this.price.interval;
}

get businessSeats() {
return {
minimumSeats: this.price.minimumSeats,
maximumSeats: this.price.maximumSeats,
};
}

get decimalAmount() {
return this.price.decimalAmount;
}

get type() {
return this.price.type;
}

get commitmentPlan() {
return this.price.commitmentPlan;
}
}
3 changes: 3 additions & 0 deletions src/infrastructure/domain/ports/payments.adapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Customer, CreateCustomerParams, UpdateCustomerParams } from '../entities/customer';
import { PaymentMethod } from '../entities/paymentMethod';
import { Price } from '../entities/price';

export interface PaymentsAdapter {
createCustomer: (params: CreateCustomerParams) => Promise<Customer>;
updateCustomer: (customerId: Customer['id'], params: Partial<UpdateCustomerParams>) => Promise<Customer>;
getCustomer: (customerId: Customer['id']) => Promise<Customer>;
searchCustomer: (email: Customer['email']) => Promise<Customer[]>;
retrievePaymentMethod: (paymentMethodId: PaymentMethod['id']) => Promise<PaymentMethod>;
getPriceById: (priceId: Price['id'], currency: string) => Promise<Price>;
getPrices: (currency: string) => Promise<Price[]>;
}
6 changes: 3 additions & 3 deletions src/services/licenseCodes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LicenseCodesRepository } from '../core/users/LicenseCodeRepository';
import { User } from '../core/users/User';
import { PaymentService } from './payment.service';
import { UsersService } from './users.service';
import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter';
import { stripeAdapter } from '../infrastructure/adapters/stripe.adapter';
import { Customer } from '../infrastructure/domain/entities/customer';
import { InvalidLicenseCodeError } from '../errors/LicenseCodeErrors';

Expand Down Expand Up @@ -73,9 +73,9 @@ export class LicenseCodesService {
let customer: Customer;

if (maybeExistingUser) {
customer = await stripePaymentsAdapter.getCustomer(maybeExistingUser.customerId);
customer = await stripeAdapter.getCustomer(maybeExistingUser.customerId);
} else {
customer = await stripePaymentsAdapter.createCustomer({
customer = await stripeAdapter.createCustomer({
name: user.name || 'Internxt User',
email: user.email,
});
Expand Down
Loading
Loading