Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
201 changes: 95 additions & 106 deletions src/controller/payments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
NotFoundPromoCodeByNameError,
PaymentService,
PromoCodeIsNotValidError,
UserAlreadyExistsError,
CustomerNotFoundError,
InvalidTaxIdError,
} from '../services/payment.service';
Expand All @@ -32,6 +31,7 @@ import { assertUser } from '../utils/assertUser';
import { fetchUserStorage } from '../utils/fetchUserStorage';
import { TierNotFoundError, TiersService } from '../services/tiers.service';
import { CustomerSyncService } from '../services/customerSync.service';
import { ForbiddenError } from '../errors/Errors';

type AllowedMethods = 'GET' | 'POST';

Expand All @@ -45,10 +45,6 @@ const ALLOWED_PATHS: {
'/plan-by-id': ['GET'],
'/promo-code-by-name': ['GET'],
'/promo-code-info': ['GET'],
'/object-storage-plan-by-id': ['GET'],
'/create-customer-for-object-storage': ['POST'],
'/payment-intent-for-object-storage': ['GET'],
'/create-subscription-for-object-storage': ['POST'],
};

export default function (
Expand All @@ -67,36 +63,43 @@ export default function (
});
fastify.addHook('onRequest', async (request, reply) => {
try {
const skipAuth = request.routeOptions?.config?.skipAuth;
const config: { url?: string; method?: AllowedMethods } = {
url: request.url.split('?')[0],
method: request.method as AllowedMethods,
};

if (
config.method &&
config.url &&
ALLOWED_PATHS[config.url] &&
ALLOWED_PATHS[config.url].includes(config.method)
(config.method &&
config.url &&
ALLOWED_PATHS[config.url] &&
ALLOWED_PATHS[config.url].includes(config.method)) ||
skipAuth
) {
return;
}

await request.jwtVerify();
} catch (err) {
request.log.warn(`JWT verification failed with error: ${(err as Error).message}`);
reply.status(401).send();
}
});

fastify.post<{ Body: { name: string; email: string; country?: string; companyVatId?: string } }>(
'/create-customer-for-object-storage',
fastify.get<{
Querystring: { email: string; customerName: string; country: string; postalCode: string; companyVatId?: string };
}>(
'/object-storage/customer',
{
schema: {
body: {
querystring: {
type: 'object',
required: ['email', 'name'],
required: ['email', 'customerName', 'country', 'postalCode'],
properties: {
name: { type: 'string' },
email: { type: 'string' },
customerName: { type: 'string' },
country: { type: 'string' },
postalCode: { type: 'string' },
companyVatId: { type: 'string' },
},
},
Expand All @@ -106,54 +109,43 @@ export default function (
max: 5,
timeWindow: '1 hour',
},
skipAuth: true,
},
},
async (req, res) => {
const { name, email, country, companyVatId } = req.body;
let customerId: Stripe.Customer['id'];
const { email, customerName, country, postalCode, companyVatId } = req.query;

if (!email) {
return res.status(404).send({
message: 'Email should be provided',
});
}
try {
const { id } = await paymentService.createOrGetCustomer(
{
name,
email,
},
country,
companyVatId,
);
const userExists = await paymentService.getCustomerIdByEmail(email).catch((err) => {
if (err instanceof CustomerNotFoundError) {
return null;
}

const token = jwt.sign(
{
customerId: id,
},
config.JWT_SECRET,
);
throw err;
});

return res.send({
customerId: id,
token,
if (userExists) {
customerId = userExists.id;
} else {
const { id } = await paymentService.createCustomer({
name: customerName,
email,
address: {
country,
postal_code: postalCode,
},
});
} catch (err) {
const error = err as Error;
if (err instanceof UserAlreadyExistsError) {
return res.status(409).send(err.message);
}
if (err instanceof InvalidTaxIdError) {
return res.status(400).send({
message: error.message,
});

if (country && companyVatId) {
await paymentService.getVatIdAndAttachTaxIdToCustomer(id, country, companyVatId);
}
req.log.error(
`[OBJECT_STORAGE_CREATE_CUSTOMER_ERROR] Customer Email: ${email} - Error: ${error.stack ?? error.message}`,
);
return res.status(500).send({
message: 'Internal Server Error',
});

customerId = id;
}

const token = jwt.sign({ customerId }, config.JWT_SECRET);

return res.send({ customerId, token });
},
);

Expand Down Expand Up @@ -375,16 +367,14 @@ export default function (
currency: string;
token: string;
promoCodeId?: string;
companyName: string;
companyVatId: string;
};
}>(
'/create-subscription-for-object-storage',
'/object-storage/subscription',
{
schema: {
body: {
type: 'object',
required: ['customerId', 'priceId'],
required: ['customerId', 'priceId', 'token'],
properties: {
customerId: {
type: 'string',
Expand All @@ -401,22 +391,15 @@ export default function (
promoCodeId: {
type: 'string',
},
companyName: {
type: 'string',
},
companyVatId: {
type: 'string',
},
},
},
},
config: {
skipAuth: true,
},
},
async (req, res) => {
const { customerId, priceId, currency, token, promoCodeId, companyName, companyVatId } = req.body;

if (!customerId || !priceId) {
throw new MissingParametersError(['customerId', 'priceId']);
}
const { customerId, priceId, currency, token, promoCodeId } = req.body;

try {
const payload = jwt.verify(token, config.JWT_SECRET) as {
Expand All @@ -425,30 +408,29 @@ export default function (
const tokenCustomerId = payload.customerId;

if (customerId !== tokenCustomerId) {
return res.status(403).send();
throw new ForbiddenError();
}
} catch (error) {
return res.status(403).send();
} catch {
throw new ForbiddenError();
}

try {
const subscriptionSetUp = await paymentService.createSubscription({
const createdSubscription = await paymentService.createSubscription({
customerId,
priceId,
currency,
companyName,
promoCodeId,
companyVatId,
additionalOptions: {
automatic_tax: {
enabled: true,
},
},
});

return res.send(subscriptionSetUp);
return res.send(createdSubscription);
} catch (err) {
const error = err as Error;
if (error instanceof MissingParametersError) {
return res.status(400).send({
message: error.message,
});
} else if (error instanceof ExistingSubscriptionError) {
if (error instanceof ExistingSubscriptionError) {
return res.status(409).send({
message: error.message,
});
Expand Down Expand Up @@ -851,6 +833,7 @@ export default function (
}
});

// TODO: Remove this useless endpoint
fastify.get<{
Querystring: {
customerId: CustomerId;
Expand Down Expand Up @@ -1136,35 +1119,41 @@ export default function (

fastify.get<{
Querystring: { planId: string; currency?: string };
schema: {
querystring: {
type: 'object';
properties: { planId: { type: 'string' }; currency: { type: 'string' } };
};
};
config: {
rateLimit: {
max: 5;
timeWindow: '1 minute';
};
};
}>('/object-storage-plan-by-id', async (req, rep) => {
const { planId, currency } = req.query;
}>(
'/object-storage/price',
{
schema: {
querystring: {
type: 'object',
properties: { planId: { type: 'string' }, currency: { type: 'string' } },
},
},
config: {
rateLimit: {
max: 5,
timeWindow: '1 minute',
},
skipAuth: true,
},
},
async (req, rep) => {
const { planId, currency } = req.query;

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

return rep.status(200).send(planObject);
} catch (error) {
const err = error as Error;
if (err instanceof NotFoundPlanByIdError) {
return rep.status(404).send({ message: err.message });
}
return rep.status(200).send(planObject);
} catch (error) {
const err = error as Error;
if (err instanceof NotFoundPlanByIdError) {
return rep.status(404).send({ message: err.message });
}

req.log.error(`[ERROR WHILE FETCHING PLAN BY ID]: ${err.message}. STACK ${err.stack ?? 'NO STACK'}`);
return rep.status(500).send({ message: 'Internal Server Error' });
}
});
req.log.error(`[ERROR WHILE FETCHING PLAN BY ID]: ${err.message}. STACK ${err.stack ?? 'NO STACK'}`);
return rep.status(500).send({ message: 'Internal Server Error' });
}
},
);

fastify.get<{
Querystring: { promotionCode: string };
Expand Down
1 change: 1 addition & 0 deletions src/services/payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export class PaymentService {
}
}

// TODO: Remove this useless function
async createOrGetCustomer(payload: Stripe.CustomerCreateParams, country?: string, companyVatId?: string) {
if (!payload.email) {
throw new MissingParametersError(['email']);
Expand Down
3 changes: 3 additions & 0 deletions tests/src/controller/checkout.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ describe('Checkout controller', () => {
});

describe('Handling promo codes', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
it('When the user provides a promo code with amount off, then the price is returned with the discount applied', async () => {
const mockedPrice = priceById({
bytes: 123456789,
Expand Down
Loading
Loading