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
27 changes: 10 additions & 17 deletions src/controller/checkout.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,17 @@ export function checkoutController(usersService: UsersService, paymentsService:
const userExists = await usersService.findUserByUuid(userUuid).catch(() => null);

if (userExists) {
await paymentsService.updateCustomer(
userExists.customerId,
{
customer: {
name: customerName,
},
},
{
email,
address: {
line1: lineAddress1,
line2: lineAddress2,
city,
postal_code: postalCode,
country,
},
await stripePaymentsAdapter.updateCustomer(userExists.customerId, {
name: customerName,
email,
address: {
line1: lineAddress1,
line2: lineAddress2,
city,
postalCode,
country,
},
);
});
customerId = userExists.customerId;
} else {
const { id } = await stripePaymentsAdapter.createCustomer({
Expand Down
3 changes: 2 additions & 1 deletion src/controller/payments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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';

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

Expand Down Expand Up @@ -334,7 +335,7 @@ export function paymentsController(
async (req, rep) => {
const user = await assertUser(req, rep, usersService);
const { address, phoneNumber } = req.body;
await paymentService.updateCustomerBillingInfo(user.customerId, {
await stripePaymentsAdapter.updateCustomer(user.customerId, {
address: {
line1: address,
},
Expand Down
25 changes: 16 additions & 9 deletions src/infrastructure/adapters/stripe.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import Stripe from 'stripe';

import { UserNotFoundError } from '../../errors/PaymentErrors';
import { PaymentsAdapter } from '../domain/ports/payments.adapter';
import { Customer, CreateCustomerParams } from '../domain/entities/customer';
import { Customer, CreateCustomerParams, UpdateCustomerParams } from '../domain/entities/customer';
import envVariablesConfig from '../../config';
import { PaymentMethod } from '../domain/entities/paymentMethod';

export class StripePaymentsAdapter implements PaymentsAdapter {
private readonly provider: Stripe = new Stripe(envVariablesConfig.STRIPE_SECRET_KEY, {
Expand All @@ -20,7 +21,7 @@ export class StripePaymentsAdapter implements PaymentsAdapter {
return Customer.toDomain(stripeCustomer);
}

async updateCustomer(customerId: Customer['id'], params: Partial<CreateCustomerParams>): Promise<Customer> {
async updateCustomer(customerId: Customer['id'], params: Partial<UpdateCustomerParams>): Promise<Customer> {
const updatedCustomer = await this.provider.customers.update(customerId, this.toStripeCustomerParams(params));

return Customer.toDomain(updatedCustomer);
Expand Down Expand Up @@ -49,18 +50,24 @@ export class StripePaymentsAdapter implements PaymentsAdapter {
return customers.data.map((customer) => Customer.toDomain(customer));
}

private toStripeCustomerParams(params: Partial<CreateCustomerParams>): Stripe.CustomerCreateParams {
async retrievePaymentMethod(paymentMethodId: PaymentMethod['id']): Promise<PaymentMethod> {
const paymentMethods = await this.provider.paymentMethods.retrieve(paymentMethodId, {});
return PaymentMethod.toDomain(paymentMethods);
}

private toStripeCustomerParams(params: Partial<UpdateCustomerParams>): Stripe.CustomerCreateParams {
return {
...(params.name && { name: params.name }),
...(params.email && { email: params.email }),
...(params.phone && { phone: params.phone }),
...(params.address && {
address: {
line1: params.address.line1,
line2: params.address.line2,
city: params.address.city,
state: params.address.state,
country: params.address.country,
postal_code: params.address.postalCode,
line1: params.address.line1 ?? undefined,
line2: params.address.line2 ?? undefined,
city: params.address.city ?? undefined,
state: params.address.state ?? undefined,
country: params.address.country ?? undefined,
postal_code: params.address.postalCode ?? undefined,
},
}),
};
Expand Down
40 changes: 18 additions & 22 deletions src/infrastructure/domain/entities/customer.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import Stripe from 'stripe';
import { BadRequestError } from '../../../errors/Errors';

export interface CustomerAddress {
line1: string;
line2: string;
city: string;
state: string;
country: string;
postalCode: string;
}
import { Address } from '../types';

export interface CreateCustomerParams {
name: string;
email: string;
address: Partial<CustomerAddress>;
address: Partial<Address>;
}

export interface UpdateCustomerParams extends Partial<CreateCustomerParams> {
phone?: string;
tax?: {
id: string;
type: Stripe.TaxIdCreateParams.Type;
};
}

export class Customer {
constructor(
public readonly id: string,
public readonly name: string,
public readonly email: string,
public readonly address: CustomerAddress,
public readonly address?: Address,
public readonly phone?: string,
) {}

Expand All @@ -34,21 +34,17 @@ export class Customer {
throw new BadRequestError('Customer email is required');
}

if (!stripeCustomer.address) {
throw new BadRequestError('Customer address is required');
}

return new Customer(
stripeCustomer.id,
stripeCustomer.name,
stripeCustomer.email,
{
line1: stripeCustomer.address.line1 ?? '',
line2: stripeCustomer.address.line2 ?? '',
city: stripeCustomer.address.city ?? '',
state: stripeCustomer.address.state ?? '',
country: stripeCustomer.address.country ?? '',
postalCode: stripeCustomer.address.postal_code ?? '',
line1: stripeCustomer.address?.line1,
line2: stripeCustomer.address?.line2,
city: stripeCustomer.address?.city,
state: stripeCustomer.address?.state,
country: stripeCustomer.address?.country,
postalCode: stripeCustomer.address?.postal_code,
},
stripeCustomer.phone ?? undefined,
);
Expand All @@ -62,7 +58,7 @@ export class Customer {
return this.email;
}

getAddress(): CustomerAddress {
getAddress(): Address | undefined {
return this.address;
}
}
28 changes: 28 additions & 0 deletions src/infrastructure/domain/entities/paymentMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Stripe from 'stripe';
import { Address } from '../types';

export class PaymentMethod {
constructor(
public readonly id: string,
public readonly address?: Address,
) {}

static toDomain(stripePaymentMethod: Stripe.PaymentMethod): PaymentMethod {
return new PaymentMethod(stripePaymentMethod.id, {
line1: stripePaymentMethod.billing_details.address?.line1,
line2: stripePaymentMethod.billing_details.address?.line2,
city: stripePaymentMethod.billing_details.address?.city,
state: stripePaymentMethod.billing_details.address?.state,
country: stripePaymentMethod.billing_details.address?.country,
postalCode: stripePaymentMethod.billing_details.address?.postal_code,
});
}

getId(): string {
return this.id;
}

getAddress(): Address | undefined {
return this.address;
}
}
6 changes: 4 additions & 2 deletions src/infrastructure/domain/ports/payments.adapter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Customer, CreateCustomerParams } from '../entities/customer';
import { Customer, CreateCustomerParams, UpdateCustomerParams } from '../entities/customer';
import { PaymentMethod } from '../entities/paymentMethod';

export interface PaymentsAdapter {
createCustomer: (params: CreateCustomerParams) => Promise<Customer>;
updateCustomer: (customerId: Customer['id'], params: Partial<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>;
}
8 changes: 8 additions & 0 deletions src/infrastructure/domain/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Address {
line1?: string | null;
line2?: string | null;
city?: string | null;
state?: string | null;
country?: string | null;
postalCode?: string | null;
}
43 changes: 2 additions & 41 deletions src/services/payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
SetupIntent,
PaymentMethod,
CustomerSource,
Customer,
Customer as StripeCustomer,
} from '../types/stripe';
import { PaymentIntent, PromotionCode, PriceByIdResponse, Reason } from '../types/payment';
import {
Expand Down Expand Up @@ -427,15 +427,6 @@ export class PaymentService {
};
}

async updateCustomerBillingInfo(
customerId: CustomerId,
payload: Pick<Stripe.CustomerUpdateParams, 'address' | 'phone'>,
): Promise<Stripe.Customer> {
const customer = await this.provider.customers.update(customerId, payload);

return customer;
}

async subscribe(
customerId: CustomerId,
priceId: PriceId,
Expand Down Expand Up @@ -725,7 +716,7 @@ export class PaymentService {
});
}

async getCustomersByEmail(customerEmail: CustomerEmail): Promise<Customer[]> {
async getCustomersByEmail(customerEmail: CustomerEmail): Promise<StripeCustomer[]> {
const res = await this.provider.customers.list({ email: customerEmail as string });

return res.data;
Expand Down Expand Up @@ -1553,36 +1544,6 @@ export class PaymentService {
});
}

async updateCustomer(
customerId: Stripe.Customer['id'],
updatableAttributes: {
customer?: Partial<Pick<Stripe.CustomerUpdateParams, 'name'>>;
tax?: {
id: string;
type: Stripe.TaxIdCreateParams.Type;
};
},
additionalOptions?: Partial<Stripe.CustomerUpdateParams>,
): Promise<void> {
if (updatableAttributes.customer && Object.keys(updatableAttributes.customer).length > 0) {
await this.provider.customers.update(customerId, {
name: updatableAttributes.customer.name,
...additionalOptions,
});
}
if (updatableAttributes.tax) {
await this.provider.taxIds.create({
owner: {
customer: customerId,
type: 'customer',
},

type: updatableAttributes.tax.type,
value: updatableAttributes.tax.id,
});
}
}

async getCryptoCurrencies() {
const currencies = await this.bit2MeService.getCurrencies();

Expand Down
19 changes: 10 additions & 9 deletions src/webhooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,19 @@ export default function (

case 'payment_intent.succeeded': {
const eventData = event.data.object;
const paymentMethod = await stripe.paymentMethods.retrieve(eventData.payment_method as string);
const userAddressBillingDetails = paymentMethod.billing_details.address;

const paymentMethod = await stripePaymentsAdapter.retrievePaymentMethod(eventData.payment_method as string);
const userAddressBillingDetails = paymentMethod.getAddress();

if (userAddressBillingDetails) {
await stripe.customers.update(eventData.customer as string, {
await stripePaymentsAdapter.updateCustomer(eventData.customer as string, {
address: {
city: userAddressBillingDetails.city as string,
line1: userAddressBillingDetails.line1 as string,
line2: userAddressBillingDetails.line2 as string,
country: userAddressBillingDetails.country as string,
postal_code: userAddressBillingDetails.postal_code as string,
state: userAddressBillingDetails.state as string,
city: userAddressBillingDetails.city,
line1: userAddressBillingDetails.line1,
line2: userAddressBillingDetails.line2,
country: userAddressBillingDetails.country,
postalCode: userAddressBillingDetails.postalCode,
state: userAddressBillingDetails.state,
},
});
}
Expand Down
35 changes: 15 additions & 20 deletions tests/src/controller/checkout.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ describe('Checkout controller', () => {

jest.spyOn(verifyRecaptcha, 'verifyRecaptcha').mockResolvedValue(true);
jest.spyOn(UsersService.prototype, 'findUserByUuid').mockResolvedValue(mockedUser);
const updateCustomerSpy = jest.spyOn(PaymentService.prototype, 'updateCustomer').mockResolvedValue();
const updateCustomerSpy = jest
.spyOn(StripePaymentsAdapter.prototype, 'updateCustomer')
.mockResolvedValue({} as any);

const response = await app.inject({
path: '/checkout/customer',
Expand All @@ -93,24 +95,17 @@ describe('Checkout controller', () => {
customerId: mockedUser.customerId,
token: userToken,
});
expect(updateCustomerSpy).toHaveBeenCalledWith(
mockedUser.customerId,
{
customer: {
name: customerData.customerName,
},
},
{
email: userEmail,
address: {
line1: customerData.lineAddress1,
line2: customerData.lineAddress2,
city: customerData.city,
postal_code: customerData.postalCode,
country: customerData.country,
},
expect(updateCustomerSpy).toHaveBeenCalledWith(mockedUser.customerId, {
name: customerData.customerName,
email: userEmail,
address: {
line1: customerData.lineAddress1,
line2: customerData.lineAddress2,
city: customerData.city,
postalCode: customerData.postalCode,
country: customerData.country,
},
);
});
});

test('when the user does not exist, then a new customer is created and saved in the database and the customer id and its token are returned', async () => {
Expand Down Expand Up @@ -192,7 +187,7 @@ describe('Checkout controller', () => {

jest.spyOn(verifyRecaptcha, 'verifyRecaptcha').mockResolvedValue(true);
jest.spyOn(UsersService.prototype, 'findUserByUuid').mockResolvedValue(mockedUser);
jest.spyOn(PaymentService.prototype, 'updateCustomer').mockResolvedValue();
jest.spyOn(StripePaymentsAdapter.prototype, 'updateCustomer').mockResolvedValue({} as any);
const attachVatIdSpy = jest
.spyOn(PaymentService.prototype, 'getVatIdAndAttachTaxIdToCustomer')
.mockResolvedValue();
Expand Down Expand Up @@ -235,7 +230,7 @@ describe('Checkout controller', () => {

jest.spyOn(verifyRecaptcha, 'verifyRecaptcha').mockResolvedValue(true);
jest.spyOn(UsersService.prototype, 'findUserByUuid').mockResolvedValue(mockedUser);
jest.spyOn(PaymentService.prototype, 'updateCustomer').mockResolvedValue();
jest.spyOn(StripePaymentsAdapter.prototype, 'updateCustomer').mockResolvedValue({} as any);
const attachVatIdSpy = jest.spyOn(PaymentService.prototype, 'getVatIdAndAttachTaxIdToCustomer');

const response = await app.inject({
Expand Down
Loading
Loading