diff --git a/src/controller/payments.controller.ts b/src/controller/payments.controller.ts index 3566b12b..e3df9d4d 100644 --- a/src/controller/payments.controller.ts +++ b/src/controller/payments.controller.ts @@ -16,7 +16,6 @@ import { NotFoundPromoCodeByNameError, PaymentService, PromoCodeIsNotValidError, - UserAlreadyExistsError, CustomerNotFoundError, InvalidTaxIdError, } from '../services/payment.service'; @@ -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'; @@ -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 ( @@ -67,18 +63,22 @@ 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}`); @@ -86,17 +86,20 @@ export default function ( } }); - 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' }, }, }, @@ -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 }); }, ); @@ -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', @@ -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 { @@ -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, }); @@ -851,6 +833,7 @@ export default function ( } }); + // TODO: Remove this useless endpoint fastify.get<{ Querystring: { customerId: CustomerId; @@ -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 }; diff --git a/src/services/payment.service.ts b/src/services/payment.service.ts index b1d256e1..623eff2f 100644 --- a/src/services/payment.service.ts +++ b/src/services/payment.service.ts @@ -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']); diff --git a/tests/src/controller/checkout.controller.test.ts b/tests/src/controller/checkout.controller.test.ts index 8d4aa841..a5c09585 100644 --- a/tests/src/controller/checkout.controller.test.ts +++ b/tests/src/controller/checkout.controller.test.ts @@ -8,12 +8,14 @@ import { getUser, getValidAuthToken, getValidUserToken, + mockCalculateTaxFor, priceById, } from '../fixtures'; import { closeServerAndDatabase, initializeServerAndDatabase } from '../utils/initializeServer'; import { UserNotFoundError, UsersService } from '../../../src/services/users.service'; import { PaymentService } from '../../../src/services/payment.service'; import { fetchUserStorage } from '../../../src/utils/fetchUserStorage'; +import Stripe from 'stripe'; jest.mock('../../../src/utils/fetchUserStorage'); @@ -470,10 +472,12 @@ describe('Checkout controller', () => { bytes: 123456789, interval: 'year', }); - const mockedTaxes = getTaxes(); + const taxes = mockCalculateTaxFor(mockedPrice.amount); jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); - jest.spyOn(PaymentService.prototype, 'calculateTax').mockResolvedValue(mockedTaxes); + jest + .spyOn(PaymentService.prototype, 'calculateTax') + .mockResolvedValue(taxes as unknown as Stripe.Tax.Calculation); const response = await app.inject({ path: `/checkout/price-by-id?priceId=${mockedPrice.id}&userAddress=123.12.12.12`, @@ -486,15 +490,18 @@ describe('Checkout controller', () => { expect(responseBody).toStrictEqual({ price: mockedPrice, taxes: { - tax: mockedTaxes.tax_amount_exclusive, - decimalTax: mockedTaxes.tax_amount_exclusive / 100, - amountWithTax: mockedTaxes.amount_total, - decimalAmountWithTax: mockedTaxes.amount_total / 100, + tax: taxes.tax_amount_exclusive, + decimalTax: taxes.tax_amount_exclusive / 100, + amountWithTax: taxes.amount_total, + decimalAmountWithTax: taxes.amount_total / 100, }, }); }); 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, @@ -506,13 +513,14 @@ describe('Checkout controller', () => { percentOff: null, codeId: 'promo_code_id', }; - const mockedTaxes = getTaxes(); - mockedTaxes.tax_amount_exclusive = mockedTaxes.tax_amount_exclusive - promoCode.amountOff; - mockedTaxes.amount_total = mockedTaxes.amount_total - promoCode.amountOff; + const discountedAmount = mockedPrice.amount - promoCode.amountOff; + const taxes = mockCalculateTaxFor(discountedAmount); jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); jest.spyOn(PaymentService.prototype, 'getPromoCodeByName').mockResolvedValue(promoCode); - jest.spyOn(PaymentService.prototype, 'calculateTax').mockResolvedValue(mockedTaxes); + jest + .spyOn(PaymentService.prototype, 'calculateTax') + .mockResolvedValue(taxes as unknown as Stripe.Tax.Calculation); const response = await app.inject({ path: `/checkout/price-by-id?priceId=${mockedPrice.id}&promoCodeName=${promoCode.promoCodeName}&userAddress=123.12.12.12`, @@ -525,10 +533,10 @@ describe('Checkout controller', () => { expect(responseBody).toStrictEqual({ price: mockedPrice, taxes: { - tax: mockedTaxes.tax_amount_exclusive, - decimalTax: mockedTaxes.tax_amount_exclusive / 100, - amountWithTax: mockedTaxes.amount_total, - decimalAmountWithTax: mockedTaxes.amount_total / 100, + tax: taxes.tax_amount_exclusive, + decimalTax: taxes.tax_amount_exclusive / 100, + amountWithTax: taxes.amount_total, + decimalAmountWithTax: taxes.amount_total / 100, }, }); }); @@ -544,16 +552,15 @@ describe('Checkout controller', () => { percentOff: 20, codeId: 'promo_code_id', }; - const mockedTaxes = getTaxes(); - const discount = Math.floor((mockedPrice.amount * promoCode.percentOff) / 100); - const discountedPrice = mockedPrice.amount - discount; - - mockedTaxes.tax_amount_exclusive = (mockedTaxes.tax_amount_exclusive * discountedPrice) / 100; - mockedTaxes.amount_total = mockedTaxes.tax_amount_exclusive + discountedPrice; + const discount = Math.floor(mockedPrice.amount * (promoCode.percentOff / 100)); + const discountedAmount = mockedPrice.amount - discount; + const taxes = mockCalculateTaxFor(discountedAmount); jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); jest.spyOn(PaymentService.prototype, 'getPromoCodeByName').mockResolvedValue(promoCode); - jest.spyOn(PaymentService.prototype, 'calculateTax').mockResolvedValue(mockedTaxes); + jest + .spyOn(PaymentService.prototype, 'calculateTax') + .mockResolvedValue(taxes as unknown as Stripe.Tax.Calculation); const response = await app.inject({ path: `/checkout/price-by-id?priceId=${mockedPrice.id}&promoCodeName=${promoCode.promoCodeName}&userAddress=123.12.12.12`, @@ -566,10 +573,10 @@ describe('Checkout controller', () => { expect(responseBody).toStrictEqual({ price: mockedPrice, taxes: { - tax: mockedTaxes.tax_amount_exclusive, - decimalTax: mockedTaxes.tax_amount_exclusive / 100, - amountWithTax: mockedTaxes.amount_total, - decimalAmountWithTax: mockedTaxes.amount_total / 100, + tax: taxes.tax_amount_exclusive, + decimalTax: taxes.tax_amount_exclusive / 100, + amountWithTax: taxes.amount_total, + decimalAmountWithTax: taxes.amount_total / 100, }, }); }); diff --git a/tests/src/controller/payments.controller.test.ts b/tests/src/controller/payments.controller.test.ts index 9a27e2ff..3c6c5661 100644 --- a/tests/src/controller/payments.controller.test.ts +++ b/tests/src/controller/payments.controller.test.ts @@ -1,19 +1,20 @@ import { FastifyInstance } from 'fastify'; import jwt from 'jsonwebtoken'; import { + getCreateSubscriptionResponse, getCustomer, getPrice, getPrices, - getPromotionCodeResponse, getUniqueCodes, getUser, getValidAuthToken, getValidUserToken, newTier, + voidPromise, } from '../fixtures'; import { closeServerAndDatabase, initializeServerAndDatabase } from '../utils/initializeServer'; import { getUserStorage } from '../../../src/services/storage.service'; -import { InvalidTaxIdError, PaymentService, UserAlreadyExistsError } from '../../../src/services/payment.service'; +import { CustomerNotFoundError, PaymentService } from '../../../src/services/payment.service'; import config from '../../../src/config'; import { HUNDRED_TB } from '../../../src/constants'; import { assertUser } from '../../../src/utils/assertUser'; @@ -329,165 +330,238 @@ describe('Payment controller e2e tests', () => { }); }); - describe('Creating a subscription for object storage', () => { - describe('Object storage subscription with promotion code', () => { - it('When the promotion code is not present, then the subscription should be created without discount/coupon', async () => { - const mockedUser = getUser(); - const mockedAuthToken = `Bearer ${getValidAuthToken(mockedUser.uuid)}`; - const token = getValidUserToken(mockedUser.customerId); + describe('Object storage tests', () => { + describe('Create customer', () => { + it('When the user exists, then its ID is returned with the user token', async () => { + const mockCustomer = getCustomer(); + const getCustomerIdSpy = jest + .spyOn(PaymentService.prototype, 'getCustomerIdByEmail') + .mockResolvedValue(mockCustomer); - const mockedBody = { - customerId: mockedUser.customerId, - priceId: 'mocked_price_id', - token, - }; - const createSubscriptionSpy = jest.spyOn(PaymentService.prototype, 'createSubscription').mockResolvedValue({ - type: 'payment', - clientSecret: 'client_secret', + const response = await app.inject({ + method: 'GET', + path: '/object-storage/customer', + query: { + customerName: mockCustomer.name as string, + email: mockCustomer.email as string, + country: mockCustomer.address?.country as string, + postalCode: mockCustomer.address?.postal_code as string, + }, + }); + + const responseBody = response.json(); + const decodedToken = jwt.verify(responseBody.token, config.JWT_SECRET) as { customerId: string }; + + expect(response.statusCode).toBe(200); + expect(responseBody.customerId).toBe(mockCustomer.id); + expect(responseBody.token).toBeDefined(); + expect(getCustomerIdSpy).toHaveBeenCalledWith(mockCustomer.email); + expect(decodedToken.customerId).toBe(mockCustomer.id); + }); + + it('When the email is missing, then an error indicating so is thrown', async () => { + const mockedCustomer = getCustomer(); + + const response = await app.inject({ + method: 'GET', + path: '/object-storage/customer', + query: { + customerName: mockedCustomer.name as string, + country: mockedCustomer.address?.country as string, + postalCode: mockedCustomer.address?.postal_code as string, + }, }); + expect(response.statusCode).toBe(400); + }); + + it('When the user does not exists, then a new one is created and the customer Id and token are provided', async () => { + const mockedCustomer = getCustomer(); + jest + .spyOn(PaymentService.prototype, 'getCustomerIdByEmail') + .mockRejectedValue(new CustomerNotFoundError('Customer not found')); + const createdCustomerSpy = jest + .spyOn(PaymentService.prototype, 'createCustomer') + .mockResolvedValue(mockedCustomer); + const response = await app.inject({ - method: 'POST', - path: '/create-subscription-for-object-storage', - body: mockedBody, - headers: { - authorization: mockedAuthToken, + method: 'GET', + path: '/object-storage/customer', + query: { + customerName: mockedCustomer.name as string, + email: mockedCustomer.email as string, + country: mockedCustomer.address?.country as string, + postalCode: mockedCustomer.address?.postal_code as string, }, }); + const responseBody = response.json(); + expect(response.statusCode).toBe(200); - expect(createSubscriptionSpy).toHaveBeenCalledWith({ - customerId: mockedBody.customerId, - priceId: mockedBody.priceId, - promoCodeId: undefined, + expect(responseBody.customerId).toBe(mockedCustomer.id); + expect(responseBody.token).toBeDefined(); + expect(createdCustomerSpy).toHaveBeenCalledWith({ + name: mockedCustomer.name, + email: mockedCustomer.email, + address: { + postal_code: mockedCustomer.address?.postal_code, + country: mockedCustomer.address?.country, + }, }); + + const decodedToken = jwt.verify(responseBody.token, config.JWT_SECRET) as { customerId: string }; + expect(decodedToken.customerId).toBe(mockedCustomer.id); }); - it('When promotion code is present, then the subscription should be created with it', async () => { - const mockedUser = getUser(); - const mockedAuthToken = `Bearer ${getValidAuthToken(mockedUser.uuid)}`; - const mockedPromoCodeId = getPromotionCodeResponse(); - const token = getValidUserToken(mockedUser.customerId); + it('When there is an unexpected error while getting the existing user, then an error indicating so is thrown', async () => { + const mockedCustomer = getCustomer(); + const unexpectedError = new Error('Random error'); + jest.spyOn(PaymentService.prototype, 'getCustomerIdByEmail').mockRejectedValue(unexpectedError); - const mockedBody = { - customerId: mockedUser.customerId, - priceId: 'mocked_price_id', - token, - promoCodeId: mockedPromoCodeId.codeId, - }; - const createSubscriptionSpy = jest.spyOn(PaymentService.prototype, 'createSubscription').mockResolvedValue({ - type: 'payment', - clientSecret: 'client_secret', + const response = await app.inject({ + method: 'GET', + path: '/object-storage/customer', + query: { + customerName: mockedCustomer.name as string, + email: mockedCustomer.email as string, + country: mockedCustomer.address?.country as string, + postalCode: mockedCustomer.address?.postal_code as string, + }, }); + expect(response.statusCode).toBe(500); + }); + + it('When the country and the tax Id are provided and is new customer, then the tax Id is attached to the customer', async () => { + const mockedCustomer = getCustomer(); + const companyVatId = 'ES123456789'; + jest + .spyOn(PaymentService.prototype, 'getCustomerIdByEmail') + .mockRejectedValue(new CustomerNotFoundError('Customer not found')); + jest.spyOn(PaymentService.prototype, 'createCustomer').mockResolvedValue(mockedCustomer); + const attachVatIdSpy = jest + .spyOn(PaymentService.prototype, 'getVatIdAndAttachTaxIdToCustomer') + .mockImplementation(voidPromise); + const response = await app.inject({ - method: 'POST', - path: '/create-subscription-for-object-storage', - body: mockedBody, - headers: { - authorization: mockedAuthToken, + method: 'GET', + path: '/object-storage/customer', + query: { + customerName: mockedCustomer.name as string, + email: mockedCustomer.email as string, + country: mockedCustomer.address?.country as string, + postalCode: mockedCustomer.address?.postal_code as string, + companyVatId, }, }); + const responseBody = response.json(); + expect(response.statusCode).toBe(200); - expect(createSubscriptionSpy).toHaveBeenCalledWith({ - customerId: mockedBody.customerId, - priceId: mockedBody.priceId, - promoCodeId: mockedBody.promoCodeId, + expect(responseBody).toStrictEqual({ + customerId: mockedCustomer.id, + token: jwt.sign({ customerId: mockedCustomer.id }, config.JWT_SECRET), }); + expect(attachVatIdSpy).toHaveBeenCalled(); + expect(attachVatIdSpy).toHaveBeenCalledWith(mockedCustomer.id, mockedCustomer.address?.country, companyVatId); }); }); - }); - describe('Create customer for object storage', () => { - it('When the user provides valid data, then a customer is created and a token is returned', async () => { - const name = 'Test User'; - const email = 'test@example.com'; - const country = 'ES'; - const customerId = 'cus_test'; - const mockCustomer = getCustomer({ id: customerId }); - const createOrGetCustomerSpy = jest - .spyOn(PaymentService.prototype, 'createOrGetCustomer') - .mockResolvedValue(mockCustomer); + describe('Create subscription', () => { + it('When the user wants to create a sub for object storage, then the subscription is created successfully with the additional taxes', async () => { + const mockedUser = getUser(); + const token = getValidUserToken(mockedUser.customerId); + const subResponse = getCreateSubscriptionResponse(); - const response = await app.inject({ - method: 'POST', - path: '/create-customer-for-object-storage', - body: { name, email, country }, - }); + const createSubscriptionSpy = jest + .spyOn(PaymentService.prototype, 'createSubscription') + .mockResolvedValue(subResponse); - const responseBody = response.json(); - expect(response.statusCode).toBe(200); - expect(createOrGetCustomerSpy).toHaveBeenCalledWith({ name, email }, country, undefined); - expect(responseBody.customerId).toBe(customerId); - expect(responseBody.token).toBeDefined(); + const body = { + customerId: mockedUser.customerId, + priceId: 'price_id', + token, + }; - const decodedToken = jwt.verify(responseBody.token, config.JWT_SECRET) as { customerId: string }; - expect(decodedToken.customerId).toBe(customerId); - }); + const response = await app.inject({ + method: 'POST', + path: '/object-storage/subscription', + body, + }); - it('When the email is missing, then it returns 404 status code', async () => { - const name = 'Test User'; - const country = 'ES'; + const responseBody = response.json(); - const response = await app.inject({ - method: 'POST', - path: '/create-customer-for-object-storage', - body: { name, country }, + expect(response.statusCode).toBe(200); + expect(responseBody).toStrictEqual(subResponse); + expect(createSubscriptionSpy).toHaveBeenCalledWith({ + customerId: mockedUser.customerId, + priceId: 'price_id', + additionalOptions: { + automatic_tax: { + enabled: true, + }, + }, + }); }); - expect(response.statusCode).toBe(400); - }); + it('When the user wants to create a subscription with promotional code, then the promotional code is applied', async () => { + const mockedUser = getUser(); + const token = getValidUserToken(mockedUser.customerId); + const promoCodeName = 'obj-sotrage-promo-code-name'; + const subResponse = getCreateSubscriptionResponse(); - it('When the user already exists, then it returns 409 status code', async () => { - const name = 'Test User'; - const email = 'existing@example.com'; - const country = 'ES'; - const userExistsError = new UserAlreadyExistsError(email); - jest.spyOn(PaymentService.prototype, 'createOrGetCustomer').mockRejectedValue(userExistsError); + const createSubscriptionSpy = jest + .spyOn(PaymentService.prototype, 'createSubscription') + .mockResolvedValue(subResponse); - const response = await app.inject({ - method: 'POST', - path: '/create-customer-for-object-storage', - body: { name, email, country }, - }); + const body = { + customerId: mockedUser.customerId, + priceId: 'price_id', + token, + promoCodeId: promoCodeName, + }; - expect(response.statusCode).toBe(409); - expect(response.body).toBe(userExistsError.message); - }); + const response = await app.inject({ + method: 'POST', + path: '/object-storage/subscription', + body, + }); - it('When the tax id is invalid, then it returns 400 status code', async () => { - const name = 'Test User'; - const email = 'invalid@example.com'; - const country = 'ES'; - const invalidTaxIdError = new InvalidTaxIdError(); - jest.spyOn(PaymentService.prototype, 'createOrGetCustomer').mockRejectedValue(invalidTaxIdError); + const responseBody = response.json(); - const response = await app.inject({ - method: 'POST', - path: '/create-customer-for-object-storage', - body: { name, email, country }, + expect(response.statusCode).toBe(200); + expect(createSubscriptionSpy).toHaveBeenCalledWith({ + customerId: mockedUser.customerId, + priceId: 'price_id', + promoCodeId: promoCodeName, + additionalOptions: { + automatic_tax: { + enabled: true, + }, + }, + }); + expect(responseBody).toStrictEqual(subResponse); }); - expect(response.statusCode).toBe(400); - expect(response.json()).toEqual({ message: invalidTaxIdError.message }); - }); + it('When the user token is not provided, then an error indicating so is thrown', async () => { + const mockedUser = getUser(); + const subResponse = getCreateSubscriptionResponse(); - it('When an internal server error occurs, then it returns 500 status code', async () => { - const name = 'Test User'; - const email = 'error@example.com'; - const country = 'ES'; - const internalError = new Error('Internal Server Error'); - jest.spyOn(PaymentService.prototype, 'createOrGetCustomer').mockRejectedValue(internalError); + jest.spyOn(PaymentService.prototype, 'createSubscription').mockResolvedValue(subResponse); - const response = await app.inject({ - method: 'POST', - path: '/create-customer-for-object-storage', - body: { name, email, country }, - }); + const body = { + customerId: mockedUser.customerId, + priceId: 'price_id', + }; + + const response = await app.inject({ + method: 'POST', + path: '/object-storage/subscription', + body, + }); - expect(response.statusCode).toBe(500); - expect(response.json()).toEqual({ message: 'Internal Server Error' }); + expect(response.statusCode).toBe(400); + }); }); }); }); diff --git a/tests/src/fixtures.ts b/tests/src/fixtures.ts index b3dca65b..eb3975ef 100644 --- a/tests/src/fixtures.ts +++ b/tests/src/fixtures.ts @@ -37,7 +37,14 @@ export const getCustomer = (params?: Partial): Stripe.Customer return { id: `cus_${randomDataGenerator.string({ length: 20 })}`, object: 'customer', - address: null, + address: { + postal_code: '123456', + country: 'ES', + city: 'Valencia', + line1: 'Avenida el Port', + line2: 'Angels', + state: 'Valencia', + }, balance: 0, created: 1680893993, currency: null, @@ -1061,3 +1068,11 @@ export const getPaymentMethod = (params?: Partial): Stripe }; export const voidPromise = () => Promise.resolve(); + +export const mockCalculateTaxFor = (amount: number, taxRate = 0.21) => { + const tax = Math.floor(amount * taxRate); + return { + tax_amount_exclusive: tax, + amount_total: amount + tax, + }; +};