diff --git a/src/controller/checkout.controller.ts b/src/controller/checkout.controller.ts index c6e2c9b1..cd74f1a8 100644 --- a/src/controller/checkout.controller.ts +++ b/src/controller/checkout.controller.ts @@ -13,7 +13,6 @@ import { signUserToken } from '../utils/signUserToken'; import { verifyRecaptcha } from '../utils/verifyRecaptcha'; import { setupAuth } from '../plugins/auth'; import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter'; -import { UserType } from '../core/users/User'; export function checkoutController(usersService: UsersService, paymentsService: PaymentService) { return async function (fastify: FastifyInstance) { @@ -190,9 +189,9 @@ export function checkoutController(usersService: UsersService, paymentsService: throw new ForbiddenError(); } - const price = await paymentsService.getPriceById(priceId); + const price = await stripePaymentsAdapter.getPriceById(priceId); - if (price.type === UserType.Business) throw new BadRequestError('Business plan is no longer available'); + if (price.isBusinessPlan()) throw new BadRequestError('Business plan is no longer available'); const subscriptionAttempt = await paymentsService.createSubscription({ customerId, @@ -286,7 +285,7 @@ export function checkoutController(usersService: UsersService, paymentsService: throw new ForbiddenError(); } - const price = await paymentsService.getPriceById(priceId); + const price = await stripePaymentsAdapter.getPriceById(priceId); if (price.interval !== 'lifetime') { throw new BadRequestError('Only lifetime plans are supported'); @@ -362,12 +361,12 @@ 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 stripePaymentsAdapter.getPriceById(priceId, currency); let amount = price.amount; if (promoCodeName) { - const couponCode = await paymentsService.getPromoCodeByName(price.product, promoCodeName); + const couponCode = await paymentsService.getPromoCodeByName(price.productId, promoCodeName); if (couponCode.amountOff) { amount = Math.max(0, price.amount - couponCode.amountOff); } else if (couponCode.percentOff) { @@ -393,7 +392,7 @@ export function checkoutController(usersService: UsersService, paymentsService: const amountTotal = taxForPrice?.amount_total ?? price.amount; return res.status(200).send({ - price, + price: price.toJSON(), taxes: { tax: taxAmount, decimalTax: taxAmount / 100, diff --git a/src/controller/payments.controller.ts b/src/controller/payments.controller.ts index 71db6772..406f938f 100644 --- a/src/controller/payments.controller.ts +++ b/src/controller/payments.controller.ts @@ -425,7 +425,6 @@ 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); @@ -433,7 +432,18 @@ export function paymentsController( return rep.status(400).send({ message: errorMessage }); } - return paymentService.getPrices(currencyValue, userType); + const prices = await stripePaymentsAdapter.getPrices(currencyValue); + + const mappedPrices = prices.map((price) => ({ + id: price.id, + productId: price.productId, + currency: price.currency, + amount: price.amount, + bytes: price.bytes, + interval: price?.interval, + })); + + return mappedPrices; }, ); diff --git a/src/infrastructure/adapters/stripe.adapter.ts b/src/infrastructure/adapters/stripe.adapter.ts index 0b011756..dc5cde0e 100644 --- a/src/infrastructure/adapters/stripe.adapter.ts +++ b/src/infrastructure/adapters/stripe.adapter.ts @@ -56,28 +56,31 @@ export class StripePaymentsAdapter implements PaymentsAdapter { async getPrices(currency: string = 'eur'): Promise { const prices = await this.provider.prices.search({ - query: `metadata["show"]:"1" active:"true" currency:"${currency}"`, + query: `metadata["show"]:"1" active:"true" currency:"eur"`, expand: ['data.currency_options', 'data.product'], limit: 100, }); - return prices.data.map((price) => { - const businessSeats = this.getBusinessSeats(price); - - 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), - recurring: price.type === 'recurring', - 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, - ...businessSeats, + return prices.data + .filter((price) => price.metadata.maxSpaceBytes && price.currency_options) + .map((price) => { + const businessSeats = this.getBusinessSeats(price); + const currencyOptions = price.currency_options![currency] ?? price.currency_options!['eur']; + + return Price.toDomain({ + id: price.id, + productId: (price.product as Stripe.Product).id, + bytes: Number.parseInt(price.metadata.maxSpaceBytes), + interval: this.getInterval(price.recurring?.interval), + commitmentPlan: this.hasAnnualCommitment(price), + recurring: price.type === 'recurring', + amount: currencyOptions.unit_amount as number, + currency, + decimalAmount: (currencyOptions.unit_amount as number) / 100, + type: price.metadata.type === 'business' ? UserType.Business : UserType.Individual, + ...businessSeats, + }); }); - }); } async getPriceById(priceId: Price['id'], currency: string = 'eur'): Promise { @@ -92,12 +95,12 @@ export class StripePaymentsAdapter implements PaymentsAdapter { return Price.toDomain({ id: price.id, productId: (price.product as Stripe.Product).id, - bytes: Number.parseInt(price.metadata.bytes), + bytes: Number.parseInt(price.metadata.maxSpaceBytes), interval: this.getInterval(price.recurring?.interval), commitmentPlan: this.hasAnnualCommitment(price), recurring: price.type === 'recurring', amount: price.currency_options![currency].unit_amount as number, - currency: price.currency, + currency, decimalAmount: (price.currency_options![currency].unit_amount as number) / 100, type: isBusinessPlan ? UserType.Business : UserType.Individual, ...businessSeats, diff --git a/src/infrastructure/domain/entities/price.ts b/src/infrastructure/domain/entities/price.ts index 527a97f2..338289b6 100644 --- a/src/infrastructure/domain/entities/price.ts +++ b/src/infrastructure/domain/entities/price.ts @@ -42,8 +42,7 @@ export class Price implements PriceAttributes { this.decimalAmount = attributes.decimalAmount; this.recurring = attributes.recurring; this.type = attributes.type; - this.minimumSeats = attributes.minimumSeats; - this.maximumSeats = attributes.maximumSeats; + this.buildBusinessSeats(attributes.minimumSeats, attributes.maximumSeats); } static toDomain(attributes: PriceAttributes): Price { @@ -61,4 +60,26 @@ export class Price implements PriceAttributes { public isRecurring(): boolean { return this.recurring; } + + public toJSON(): PriceAttributes { + return { + id: this.id, + productId: this.productId, + bytes: this.bytes, + interval: this.interval, + commitmentPlan: this.commitmentPlan, + recurring: this.recurring, + amount: this.amount, + currency: this.currency, + decimalAmount: this.decimalAmount, + type: this.type, + ...(this.minimumSeats !== undefined && { minimumSeats: this.minimumSeats }), + ...(this.maximumSeats !== undefined && { maximumSeats: this.maximumSeats }), + }; + } + + private buildBusinessSeats(minimumSeats?: number, maximumSeats?: number) { + if (minimumSeats !== undefined) this.minimumSeats = minimumSeats; + if (maximumSeats !== undefined) this.maximumSeats = maximumSeats; + } } diff --git a/src/services/payment.service.ts b/src/services/payment.service.ts index 2243548c..96dede97 100644 --- a/src/services/payment.service.ts +++ b/src/services/payment.service.ts @@ -1,7 +1,6 @@ import Stripe from 'stripe'; import dayjs from 'dayjs'; -import { DisplayPrice } from '../core/users/DisplayPrice'; import { ProductsRepository } from '../core/users/ProductsRepository'; import { UserSubscription, UserType } from '../core/users/User'; import { Bit2MeService } from './bit2me.service'; @@ -35,7 +34,7 @@ import { CustomerSource, Customer as StripeCustomer, } from '../types/stripe'; -import { PaymentIntent, PromotionCode, PriceByIdResponse } from '../types/payment'; +import { PaymentIntent, PromotionCode } from '../types/payment'; import { RenewalPeriod, PlanSubscription, SubscriptionCreated } from '../types/subscription'; import { stripePaymentsAdapter } from '../infrastructure/adapters/stripe.adapter'; @@ -190,10 +189,6 @@ export class PaymentService { } } - async getPrice(priceId: string): Promise { - return this.provider.prices.retrieve(priceId); - } - /** * Creates an invoice to purchase a one time plan. * @@ -261,8 +256,8 @@ export class PaymentService { throw new BadRequestError('Invoice item does not have a price.'); } - const price = await this.getPrice(invoiceItem.pricing?.price_details?.price); - const isLifetime = price.type === 'one_time'; + const price = await stripePaymentsAdapter.getPriceById(invoiceItem.pricing?.price_details?.price); + const isLifetime = price.interval === 'lifetime'; if (isLifetime && isCryptoCurrency(currency)) { const normalizedCurrencyForBit2Me = normalizeForBit2Me(currency); @@ -903,101 +898,6 @@ export class PaymentService { }; } - async getPrices(currency?: string, userType: UserType = UserType.Individual): Promise { - const currencyValue = currency ?? 'eur'; - - const res = await this.provider.prices.search({ - query: `metadata["show"]:"1" active:"true" currency:"${currencyValue}"`, - expand: ['data.currency_options', 'data.product'], - limit: 100, - }); - - return res.data - .filter((price) => { - const priceProductType = ((price.product as Stripe.Product).metadata.type as UserType) || UserType.Individual; - return ( - price.metadata.maxSpaceBytes && - price.currency_options && - price.currency_options[currencyValue].unit_amount && - priceProductType === userType - ); - }) - .map((price) => { - const hasAnnualCommitment = this.hasAnnualCommitment(price); - const recurringInterval = hasAnnualCommitment ? 'year' : (price.recurring?.interval as 'year' | 'month'); - - return { - id: price.id, - productId: (price.product as Stripe.Product).id, - currency: currencyValue, - amount: price.currency_options![currencyValue].unit_amount as number, - bytes: parseInt(price.metadata.maxSpaceBytes), - interval: price.type === 'one_time' ? 'lifetime' : recurringInterval, - }; - }); - } - - async getPricesRaw(currency?: string, expandProduct = false): Promise { - const currencyValue = currency ?? 'eur'; - - const expandOptions = ['data.currency_options']; - - if (expandProduct) { - expandOptions.push('data.product'); - } - - //!TODO: add metadata["show"]:"1" in query param - const res = await this.provider.prices.search({ - query: `active:"true" currency:"${currencyValue}"`, - expand: expandOptions, - limit: 100, - }); - - return res.data.filter( - (price) => - price.metadata.maxSpaceBytes && price.currency_options && price.currency_options[currencyValue].unit_amount, - ); - } - - /** - * Returns the requested price if exists - * @param priceId - The id of the requested price - * @param currency - The currency of the requested price - * @returns - The selected price if it exists and it is active - */ - async getPriceById(priceId: string, currency = 'eur'): Promise { - const availablePrices = await this.getPricesRaw(currency); - - const selectedPrice = availablePrices.find((price) => price.id === priceId && price.active); - - if (!selectedPrice) { - throw new NotFoundError('The requested price does not exist'); - } - - let businessSeats; - const { currency_options, recurring, metadata, type, product } = selectedPrice; - const isBusinessPrice = metadata?.type === 'business'; - - if (isBusinessPrice) { - businessSeats = { - minimumSeats: Number(metadata.minimumSeats), - maximumSeats: Number(metadata.maximumSeats), - }; - } - - return { - id: priceId, - currency, - amount: currency_options![currency].unit_amount as number, - bytes: Number.parseInt(metadata?.maxSpaceBytes), - interval: type === 'one_time' ? 'lifetime' : recurring?.interval, - decimalAmount: (currency_options![currency].unit_amount as number) / 100, - type: isBusinessPrice ? UserType.Business : UserType.Individual, - product: product as string, - ...businessSeats, - }; - } - /** * Returns the tax for a given price * @param priceId - The Id of the price that we want to calculate the tax for diff --git a/tests/src/controller/checkout.controller.test.ts b/tests/src/controller/checkout.controller.test.ts index ddd9d230..ee90ddc7 100644 --- a/tests/src/controller/checkout.controller.test.ts +++ b/tests/src/controller/checkout.controller.test.ts @@ -5,13 +5,13 @@ import { getCustomer, getInvoice, getPrice, + getPriceEntity, getRawCryptoInvoiceResponse, getTaxes, getUser, getValidAuthToken, getValidUserToken, mockCalculateTaxFor, - priceById, } from '../fixtures'; import { closeServerAndDatabase, initializeServerAndDatabase } from '../utils/initializeServer'; import { UsersService } from '../../../src/services/users.service'; @@ -288,9 +288,9 @@ describe('Checkout controller', () => { const authToken = getValidAuthToken(mockedUser.uuid); const userToken = getValidUserToken({ customerId: mockedUser.customerId }); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue({ - type: UserType.Individual, - } as any); + jest + .spyOn(StripePaymentsAdapter.prototype, 'getPriceById') + .mockResolvedValue(getPriceEntity({ type: UserType.Individual })); jest.spyOn(PaymentService.prototype, 'createSubscription').mockResolvedValue(mockedSubscriptionResponse); jest.spyOn(verifyRecaptcha, 'verifyRecaptcha').mockResolvedValue(true); @@ -325,9 +325,9 @@ describe('Checkout controller', () => { const authToken = getValidAuthToken(mockedUser.uuid); const userToken = getValidUserToken({ customerId: mockedUser.customerId }); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue({ - type: UserType.Business, - } as any); + jest + .spyOn(StripePaymentsAdapter.prototype, 'getPriceById') + .mockResolvedValue(getPriceEntity({ type: UserType.Business })); jest.spyOn(verifyRecaptcha, 'verifyRecaptcha').mockResolvedValue(true); const response = await app.inject({ @@ -492,8 +492,7 @@ describe('Checkout controller', () => { 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({ - bytes: 123456789, + const mockedPrice = getPriceEntity({ interval: 'lifetime', }); const authToken = getValidAuthToken(mockedUser.uuid); @@ -505,7 +504,7 @@ describe('Checkout controller', () => { } as const; const mockedCaptchaToken = 'captcha_token'; - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); (fetchUserStorage as jest.Mock).mockResolvedValue({ canExpand: true, }); @@ -536,7 +535,7 @@ describe('Checkout controller', () => { test('when the user want to pay a one time plan using crypto currencies, then an invoice is created and the specific payload containing the QR Link is returned', async () => { const mockedUser = getUser(); const mockedInvoice = getInvoice(); - const mockedPrice = priceById({ + const mockedPrice = getPriceEntity({ bytes: 123456789, interval: 'lifetime', }); @@ -557,7 +556,7 @@ describe('Checkout controller', () => { token: getValidUserToken({ invoiceId: 'invoice_id' }), } as const; - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); (fetchUserStorage as jest.Mock).mockResolvedValue({ canExpand: true, }); @@ -588,13 +587,13 @@ describe('Checkout controller', () => { test('When the user wants to pay a subscription plan creating an invoice, then an error indicating so is thrown', async () => { const mockedUser = getUser(); - const mockedPrice = priceById({ + const mockedPrice = getPriceEntity({ bytes: 123456789, interval: 'year', }); const authToken = getValidAuthToken(mockedUser.uuid); const userToken = getValidUserToken({ customerId: mockedUser.customerId }); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); const response = await app.inject({ path: '/checkout/payment-intent', @@ -616,14 +615,14 @@ describe('Checkout controller', () => { 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({ + const mockedPrice = getPriceEntity({ bytes: 123456789, interval: 'lifetime', }); const authToken = getValidAuthToken(mockedUser.uuid); const userToken = getValidUserToken({ customerId: mockedUser.customerId }); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); (fetchUserStorage as jest.Mock).mockResolvedValue({ canExpand: false, }); @@ -826,13 +825,13 @@ describe('Checkout controller', () => { describe('Get Price by its ID', () => { test('When the user wants to get a price by its ID, then the price is returned with its taxes', async () => { - const mockedPrice = priceById({ + const mockedPrice = getPriceEntity({ bytes: 123456789, interval: 'year', }); const taxes = mockCalculateTaxFor(mockedPrice.amount); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); jest .spyOn(PaymentService.prototype, 'calculateTax') .mockResolvedValueOnce(taxes as unknown as Stripe.Tax.Calculation); @@ -850,7 +849,7 @@ describe('Checkout controller', () => { expect(response.statusCode).toBe(200); expect(responseBody).toStrictEqual({ - price: mockedPrice, + price: mockedPrice.toJSON(), taxes: { tax: taxes.tax_amount_exclusive, decimalTax: taxes.tax_amount_exclusive / 100, @@ -862,7 +861,7 @@ describe('Checkout controller', () => { describe('Handling promo codes', () => { test('When the user provides a promo code with amount off, then the price is returned with the discount applied', async () => { - const mockedPrice = priceById({ + const mockedPrice = getPriceEntity({ bytes: 123456789, interval: 'year', }); @@ -875,7 +874,7 @@ describe('Checkout controller', () => { const discountedAmount = mockedPrice.amount - promoCode.amountOff; const taxes = mockCalculateTaxFor(discountedAmount); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); jest.spyOn(PaymentService.prototype, 'getPromoCodeByName').mockResolvedValue(promoCode); jest .spyOn(PaymentService.prototype, 'calculateTax') @@ -895,7 +894,7 @@ describe('Checkout controller', () => { expect(response.statusCode).toBe(200); expect(responseBody).toStrictEqual({ - price: mockedPrice, + price: mockedPrice.toJSON(), taxes: { tax: taxes.tax_amount_exclusive, decimalTax: taxes.tax_amount_exclusive / 100, @@ -906,7 +905,7 @@ describe('Checkout controller', () => { }); test('When the user provides a promo code with percent off, then the price is returned with the discount applied', async () => { - const mockedPrice = priceById({ + const mockedPrice = getPriceEntity({ bytes: 123456789, interval: 'year', }); @@ -920,7 +919,7 @@ describe('Checkout controller', () => { const discountedAmount = mockedPrice.amount - discount; const taxes = mockCalculateTaxFor(discountedAmount); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); jest.spyOn(PaymentService.prototype, 'getPromoCodeByName').mockResolvedValue(promoCode); jest .spyOn(PaymentService.prototype, 'calculateTax') @@ -940,7 +939,7 @@ describe('Checkout controller', () => { expect(response.statusCode).toBe(200); expect(responseBody).toStrictEqual({ - price: mockedPrice, + price: mockedPrice.toJSON(), taxes: { tax: taxes.tax_amount_exclusive, decimalTax: taxes.tax_amount_exclusive / 100, @@ -951,14 +950,12 @@ 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 () => { - const mockedPrice = { - ...priceById({ - bytes: 123456789, - interval: 'year', - }), + const mockedPrice = getPriceEntity({ + bytes: 123456789, + interval: 'year', amount: 14000, decimalAmount: 140, - }; + }); const promoCode = { promoCodeName: 'promo_code_name', amountOff: 15000, @@ -969,7 +966,7 @@ describe('Checkout controller', () => { const discountedAmount = 0; const taxes = mockCalculateTaxFor(discountedAmount); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); jest.spyOn(PaymentService.prototype, 'getPromoCodeByName').mockResolvedValue(promoCode); jest .spyOn(PaymentService.prototype, 'calculateTax') @@ -989,7 +986,7 @@ describe('Checkout controller', () => { expect(response.statusCode).toBe(200); expect(responseBody).toStrictEqual({ - price: mockedPrice, + price: mockedPrice.toJSON(), taxes: { tax: taxes.tax_amount_exclusive, decimalTax: taxes.tax_amount_exclusive / 100, @@ -1013,13 +1010,13 @@ describe('Checkout controller', () => { describe('User address, country and postal code are not provided', () => { test('When any of user location params are provided, then the price is returned with taxes to 0', async () => { - const mockedPrice = priceById({ + const mockedPrice = getPriceEntity({ bytes: 123456789, interval: 'year', }); const mockedTaxes = getTaxes(); - jest.spyOn(PaymentService.prototype, 'getPriceById').mockResolvedValue(mockedPrice); + jest.spyOn(StripePaymentsAdapter.prototype, 'getPriceById').mockResolvedValue(mockedPrice); jest.spyOn(PaymentService.prototype, 'calculateTax').mockResolvedValue(mockedTaxes); const response = await app.inject({ @@ -1034,7 +1031,7 @@ describe('Checkout controller', () => { expect(response.statusCode).toBe(200); expect(responseBody).toStrictEqual({ - price: mockedPrice, + price: mockedPrice.toJSON(), taxes: { tax: 0, decimalTax: 0, diff --git a/tests/src/controller/payments.controller.test.ts b/tests/src/controller/payments.controller.test.ts index 078424bd..cc190f8b 100644 --- a/tests/src/controller/payments.controller.test.ts +++ b/tests/src/controller/payments.controller.test.ts @@ -5,6 +5,7 @@ import { getCustomer, getLicenseCode, getPaymentIntent, + getPriceEntity, getUniqueCodes, getUser, getValidAuthToken, @@ -544,4 +545,30 @@ describe('Payment controller e2e tests', () => { }); }); }); + + describe('Get prices', () => { + test('When fetching the available prices, then they are returned with the necessary data', async () => { + const mockedPriceEntity = getPriceEntity(); + const expectedResponse = { + id: mockedPriceEntity.id, + currency: mockedPriceEntity.currency, + amount: mockedPriceEntity.amount, + bytes: mockedPriceEntity.bytes, + interval: mockedPriceEntity.interval, + productId: mockedPriceEntity.productId, + }; + + jest.spyOn(StripePaymentsAdapter.prototype, 'getPrices').mockResolvedValue([mockedPriceEntity]); + + const response = await app.inject({ + method: 'GET', + path: '/prices', + }); + + const responseBody = response.json(); + + expect(response.statusCode).toBe(200); + expect(responseBody).toStrictEqual([expectedResponse]); + }); + }); }); diff --git a/tests/src/infrastructure/adapters/stripe.adapter.test.ts b/tests/src/infrastructure/adapters/stripe.adapter.test.ts index a443412a..bfac369e 100644 --- a/tests/src/infrastructure/adapters/stripe.adapter.test.ts +++ b/tests/src/infrastructure/adapters/stripe.adapter.test.ts @@ -180,7 +180,7 @@ describe('Stripe Adapter', () => { describe('Get prices', () => { test('When getting all available prices, then all prices are returned with the correct data', async () => { const stripePrice = getPrice({ - metadata: { show: '1', bytes: '107374182400', type: 'individual', annualCommitment: 'false' }, + metadata: { show: '1', maxSpaceBytes: '107374182400', type: 'individual', annualCommitment: 'false' }, recurring: { interval: 'year', interval_count: 1, @@ -206,7 +206,7 @@ describe('Stripe Adapter', () => { Price.toDomain({ id: stripePrice.id, productId: 'prod_test', - bytes: 107374182400, + bytes: Number(stripePrice.metadata.maxSpaceBytes), interval: 'year', commitmentPlan: false, recurring: true, @@ -222,7 +222,7 @@ describe('Stripe Adapter', () => { const stripePrice = getPrice({ metadata: { show: '1', - bytes: '107374182400', + maxSpaceBytes: '107374182400', type: 'business', annualCommitment: 'false', minimumSeats: '1', @@ -257,7 +257,7 @@ describe('Stripe Adapter', () => { describe('Get price by ID', () => { test('When getting a price by its ID, then the price is returned with the correct data', async () => { const stripePrice = getPrice({ - metadata: { bytes: '107374182400', type: 'individual', annualCommitment: 'false' }, + metadata: { maxSpaceBytes: '107374182400', type: 'individual', annualCommitment: 'false' }, recurring: { interval: 'year', interval_count: 1, @@ -280,7 +280,7 @@ describe('Stripe Adapter', () => { Price.toDomain({ id: stripePrice.id, productId: 'prod_test', - bytes: 107374182400, + bytes: Number(stripePrice.metadata.maxSpaceBytes), interval: 'year', commitmentPlan: false, recurring: true, diff --git a/tests/src/services/payment.service.test.ts b/tests/src/services/payment.service.test.ts index 2d52cdb9..9292625a 100644 --- a/tests/src/services/payment.service.test.ts +++ b/tests/src/services/payment.service.test.ts @@ -15,6 +15,7 @@ import { getPaymentIntentResponse, getPaymentMethod, getPrice, + getPriceEntity, getPrices, getProduct, getPromoCode, @@ -112,18 +113,6 @@ describe('Payments Service tests', () => { }); }); - describe('Get a price given its ID', () => { - it('When the price exists, then it is returned', async () => { - const mockedPrice = getPrice(); - const priceSpy = jest.spyOn(paymentService, 'getPrice').mockResolvedValue(mockedPrice); - - const price = await paymentService.getPrice(mockedPrice.id); - - expect(priceSpy).toHaveBeenCalledWith(mockedPrice.id); - expect(price).toEqual(mockedPrice); - }); - }); - describe('Creating an invoice', () => { test('When trying to create an invoice with the correct params, then it is successfully created', async () => { const mockedPaymentIntent = getPaymentIntentResponse({ type: 'fiat' }); @@ -187,7 +176,7 @@ describe('Payments Service tests', () => { client_secret: mockedPaymentIntent.clientSecret as string, }, }); - const mockedPrice = getPrice({ + const mockedPrice = getPriceEntity({ id: 'mockedPriceId', }); @@ -197,7 +186,7 @@ describe('Payments Service tests', () => { jest .spyOn(stripeNewVersion.invoiceItems, 'create') .mockResolvedValueOnce(mockedInvoice.lines.data[0] as unknown as Stripe.Response); - jest.spyOn(paymentService, 'getPrice').mockResolvedValueOnce(mockedPrice); + jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValueOnce(mockedPrice); jest .spyOn(stripeNewVersion.invoices, 'finalizeInvoice') .mockResolvedValueOnce(mockedInvoice as unknown as Stripe.Response); @@ -237,7 +226,7 @@ describe('Payments Service tests', () => { ], }, }); - const mockedPrice = getPrice({ + const mockedPrice = getPriceEntity({ id: 'mockedPriceId', }); @@ -254,7 +243,7 @@ describe('Payments Service tests', () => { jest .spyOn(stripeNewVersion.invoiceItems, 'create') .mockResolvedValueOnce(mockedInvoice.lines.data[0] as unknown as Stripe.Response); - jest.spyOn(paymentService, 'getPrice').mockResolvedValueOnce(mockedPrice); + jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValueOnce(mockedPrice); jest .spyOn(stripeNewVersion.invoices, 'finalizeInvoice') .mockResolvedValueOnce(mockedInvoice as unknown as Stripe.Response); @@ -281,8 +270,8 @@ describe('Payments Service tests', () => { }); const mockedCustomerEmail = mockedCustomer.email as string; const mockedCustomerId = mockedCustomer.id as string; - const mockedPrice = getPrice({ - type: 'one_time', + const mockedPrice = getPriceEntity({ + interval: 'lifetime', }); const mockedPriceId = mockedPrice.id as string; const mockInvoiceId = 'in_test_456'; @@ -319,7 +308,7 @@ describe('Payments Service tests', () => { }, }); - jest.spyOn(paymentService, 'getPrice').mockResolvedValue(mockedPrice); + jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValue(mockedPrice); jest.spyOn(stripePaymentsAdapter, 'getCustomer').mockRejectedValue(new BadRequestError()); jest .spyOn(stripeNewVersion.invoices, 'create') @@ -344,8 +333,8 @@ describe('Payments Service tests', () => { const mockedCustomer = getCustomer(); const mockedCustomerEmail = mockedCustomer.email as string; const mockedCustomerId = mockedCustomer.id as string; - const mockedPrice = getPrice({ - type: 'one_time', + const mockedPrice = getPriceEntity({ + interval: 'lifetime', }); const mockedPriceId = mockedPrice.id as string; const mockInvoiceId = 'in_test_456'; @@ -411,7 +400,7 @@ describe('Payments Service tests', () => { jest .spyOn(stripeNewVersion.invoices, 'finalizeInvoice') .mockResolvedValueOnce(mockedInvoice as unknown as Stripe.Response); - jest.spyOn(paymentService, 'getPrice').mockResolvedValue(mockedPrice); + jest.spyOn(stripePaymentsAdapter, 'getPriceById').mockResolvedValue(mockedPrice); const createCryptoInvoiceSpy = jest .spyOn(bit2MeService, 'createCryptoInvoice') .mockResolvedValueOnce(mockedParsedCreatedInvoiceResponse); @@ -708,73 +697,6 @@ describe('Payments Service tests', () => { }); }); - describe('Fetch a price by its ID', () => { - it('When the price does not exist, an error indicating so is thrown', async () => { - const mockedPrices = getPrice(); - const invalidPriceId = 'invalid_price_id'; - - jest.spyOn(paymentService, 'getPricesRaw').mockResolvedValue([mockedPrices]); - - await expect(paymentService.getPriceById(invalidPriceId)).rejects.toThrow(NotFoundError); - }); - - it('When the price exists and belongs to a business product, then the price is returned with minimum and maximum seats', async () => { - const businessSeats = { - minimumSeats: 1, - maximumSeats: 3, - }; - const mockedPrice = getPrice({ - metadata: { - type: 'business', - maxSpaceBytes: '123456789', - minimumSeats: businessSeats.minimumSeats.toString(), - maximumSeats: businessSeats.maximumSeats.toString(), - }, - }); - const validPriceId = mockedPrice.id; - const priceResponse = { - id: validPriceId, - currency: mockedPrice.currency, - amount: mockedPrice.currency_options![mockedPrice.currency].unit_amount as number, - bytes: parseInt(mockedPrice.metadata?.maxSpaceBytes), - interval: mockedPrice.type === 'one_time' ? 'lifetime' : mockedPrice.recurring?.interval, - decimalAmount: (mockedPrice.currency_options![mockedPrice.currency].unit_amount as number) / 100, - product: mockedPrice.product as string, - type: UserType.Business, - ...businessSeats, - }; - jest.spyOn(paymentService, 'getPricesRaw').mockResolvedValue([mockedPrice]); - - const price = await paymentService.getPriceById(validPriceId); - - expect(price).toStrictEqual(priceResponse); - }); - - it('When the price exists, then the correct price object is returned', async () => { - const mockedPrice = getPrice({ - metadata: { - maxSpaceBytes: '123456789', - }, - }); - const validPriceId = mockedPrice.id; - const priceResponse = { - id: validPriceId, - currency: mockedPrice.currency, - amount: mockedPrice.currency_options![mockedPrice.currency].unit_amount as number, - bytes: parseInt(mockedPrice.metadata?.maxSpaceBytes), - interval: mockedPrice.type === 'one_time' ? 'lifetime' : mockedPrice.recurring?.interval, - decimalAmount: (mockedPrice.currency_options![mockedPrice.currency].unit_amount as number) / 100, - product: mockedPrice.product as string, - type: UserType.Individual, - }; - jest.spyOn(paymentService, 'getPricesRaw').mockResolvedValue([mockedPrice]); - - const price = await paymentService.getPriceById(validPriceId); - - expect(price).toStrictEqual(priceResponse); - }); - }); - describe('Get tax for a price', () => { it('When the params are correct, then a tax object is returned for the requested price', async () => { const mockedPrice = getPrice();