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
4 changes: 2 additions & 2 deletions src/controller/checkout.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,11 +467,11 @@ export default function (usersService: UsersService, paymentsService: PaymentSer
if (promoCodeName) {
const couponCode = await paymentsService.getPromoCodeByName(price.product, promoCodeName);
if (couponCode.amountOff) {
amount = price.amount - couponCode.amountOff;
amount = Math.max(0, price.amount - couponCode.amountOff);
} else if (couponCode.percentOff) {
const discount = Math.floor((price.amount * couponCode.percentOff) / 100);
const discountedPrice = price.amount - discount;
amount = discountedPrice;
amount = Math.max(0, discountedPrice);
}
}

Expand Down
91 changes: 70 additions & 21 deletions tests/src/controller/checkout.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ beforeEach(() => {
});

describe('Checkout controller', () => {
it('When the jwt verify fails, then an error indicating so is thrown', async () => {
test('When the jwt verify fails, then an error indicating so is thrown', async () => {
const userAuthToken = 'invalid_token';

const response = await app.inject({
Expand All @@ -55,7 +55,7 @@ describe('Checkout controller', () => {
});

describe('Get customer ID', () => {
it('When the user exists in Users collection, then the customer Id associated to the user is returned', async () => {
test('When the user exists in Users collection, then the customer Id associated to the user is returned', async () => {
const mockedUser = getUser();
const userAuthToken = getValidAuthToken(mockedUser.uuid);
const userToken = getValidUserToken({ customerId: mockedUser.customerId });
Expand Down Expand Up @@ -127,7 +127,7 @@ describe('Checkout controller', () => {
expect(insertUserSpy).toHaveBeenCalledWith(insertUserPayload);
});

it('When the user provides country and Vat Id, then they are attached to the user correctly', async () => {
test('When the user provides country and Vat Id, then they are attached to the user correctly', async () => {
const country = 'ES';
const companyVatId = 'vat_id';

Expand Down Expand Up @@ -391,7 +391,7 @@ describe('Checkout controller', () => {
});

describe('Creating a subscription', () => {
it('When the user wants to create a subscription, it is created successfully', async () => {
test('When the user wants to create a subscription, test is created successfully', async () => {
const mockedUser = getUser();
const mockedSubscription = getCreatedSubscription();
const mockedSubscriptionResponse = getCreateSubscriptionResponse();
Expand Down Expand Up @@ -426,7 +426,7 @@ describe('Checkout controller', () => {
});

describe('Handling errors', () => {
it('When the id of the price is not present in the body, then an error indicating so is thrown', async () => {
test('When the id of the price is not present in the body, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);

Expand All @@ -444,7 +444,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the id of the customer is not present in the body, then an error indicating so is thrown', async () => {
test('When the id of the customer is not present in the body, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);

Expand All @@ -462,7 +462,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the user token is not present in the body, then an error indicating so is thrown', async () => {
test('When the user token is not present in the body, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);

Expand All @@ -481,7 +481,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the provided token is invalid or cannot be verified, then an error indicating so is thrown', async () => {
test('When the provided token is invalid or cannot be verified, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);
const invalidUserToken = 'malformed.token.payload';
Expand All @@ -506,7 +506,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(403);
});

it('When the provided token contains a customerId that does not match the provided customerId, then an error indicating so is thrown', async () => {
test('When the provided token contains a customerId that does not match the provided customerId, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);
const userToken = getValidUserToken({ customerId: 'invalid_customer_id' });
Expand Down Expand Up @@ -566,7 +566,7 @@ describe('Checkout controller', () => {
jest.clearAllMocks();
});

it('When the user wants to pay a one time plan, then an invoice is created and the client secret is returned', async () => {
test('When the user wants to pay a one time plan, then an invoice is created and the client secret is returned', async () => {
const mockedUser = getUser();
const mockedInvoice = getInvoice();
const mockedPrice = priceById({
Expand Down Expand Up @@ -690,7 +690,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the user already has the max storage allowed, then an error indicating so is thrown', async () => {
test('When the user already has the max storage allowed, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const mockedInvoice = getInvoice();
const mockedPrice = priceById({
Expand Down Expand Up @@ -765,7 +765,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the id of the price is not present in the body, then an error indicating so is thrown', async () => {
test('When the id of the price is not present in the body, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);

Expand All @@ -783,7 +783,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the id of the customer is not present in the body, then an error indicating so is thrown', async () => {
test('When the id of the customer is not present in the body, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);

Expand All @@ -801,7 +801,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the user token is not present in the body, then an error indicating so is thrown', async () => {
test('When the user token is not present in the body, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);

Expand All @@ -820,7 +820,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(400);
});

it('When the provided token is invalid or cannot be verified, then an error indicating so is thrown', async () => {
test('When the provided token is invalid or cannot be verified, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);
const invalidUserToken = 'malformed.token.payload';
Expand All @@ -846,7 +846,7 @@ describe('Checkout controller', () => {
expect(response.statusCode).toBe(403);
});

it('When the provided token contains a customerId that does not match the provided customerId, then an error indicating so is thrown', async () => {
test('When the provided token contains a customerId that does not match the provided customerId, then an error indicating so is thrown', async () => {
const mockedUser = getUser();
const authToken = getValidAuthToken(mockedUser.uuid);
const userToken = getValidUserToken({ invoiceId: 'invalid_customer_id' });
Expand Down Expand Up @@ -902,7 +902,7 @@ describe('Checkout controller', () => {
});

describe('Get Price by its ID', () => {
it('When the user wants to get a price by its ID, then the price is returned with its taxes', async () => {
test('When the user wants to get a price by its ID, then the price is returned with its taxes', async () => {
const mockedPrice = priceById({
bytes: 123456789,
interval: 'year',
Expand Down Expand Up @@ -938,7 +938,7 @@ describe('Checkout controller', () => {
});

describe('Handling promo codes', () => {
it('When the user provides a promo code with amount off, then the price is returned with the discount applied', async () => {
test('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,
interval: 'year',
Expand Down Expand Up @@ -982,7 +982,7 @@ describe('Checkout controller', () => {
});
});

it('When the user provides a promo code with percent off, then the price is returned with the discount applied', async () => {
test('When the user provides a promo code with percent off, then the price is returned with the discount applied', async () => {
const mockedPrice = priceById({
bytes: 123456789,
interval: 'year',
Expand Down Expand Up @@ -1026,10 +1026,59 @@ describe('Checkout controller', () => {
},
});
});

test('When the user provides a promo code with a discount that is more than the product price, then the price should be 0 instead of a negative price', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To not block you know, I will leave it as a suggestion: if the controller allows it (because it is valid), consider handling these things at the business case level the next time (services/domains).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep that in mind. Thanks for the feedback! 🙌🏽

const mockedPrice = {
...priceById({
bytes: 123456789,
interval: 'year',
}),
amount: 14000,
decimalAmount: 140,
};
const promoCode = {
promoCodeName: 'promo_code_name',
amountOff: 15000,
percentOff: null,
codeId: 'promo_code_id',
};

const discountedAmount = 0;
const taxes = mockCalculateTaxFor(discountedAmount);

jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice);
jest.spyOn(PaymentService.prototype, 'getPromoCodeByName').mockResolvedValue(promoCode);
jest
.spyOn(PaymentService.prototype, 'calculateTax')
.mockResolvedValueOnce(taxes as unknown as Stripe.Tax.Calculation);

const response = await app.inject({
path: `/checkout/price-by-id`,
query: {
priceId: mockedPrice.id,
promoCodeName: promoCode.promoCodeName,
userAddress: '123.12.12.12',
},
method: 'GET',
});

const responseBody = response.json();

expect(response.statusCode).toBe(200);
expect(responseBody).toStrictEqual({
price: mockedPrice,
taxes: {
tax: taxes.tax_amount_exclusive,
decimalTax: taxes.tax_amount_exclusive / 100,
amountWithTax: taxes.amount_total,
decimalAmountWithTax: taxes.amount_total / 100,
},
});
});
});

describe('Handling errors', () => {
it('When the priceId is not present in the query, then an error indicating so is thrown', async () => {
test('When the priceId is not present in the query, then an error indicating so is thrown', async () => {
const response = await app.inject({
path: '/checkout/price-by-id',
method: 'GET',
Expand All @@ -1040,7 +1089,7 @@ describe('Checkout controller', () => {
});

describe('User address, country and postal code are not provided', () => {
it('When any of user location params are provided, then the price is returned with taxes to 0', async () => {
test('When any of user location params are provided, then the price is returned with taxes to 0', async () => {
const mockedPrice = priceById({
bytes: 123456789,
interval: 'year',
Expand Down
Loading