diff --git a/__tests__/paddle.ts b/__tests__/paddle.ts index eea2bd622b..8e40fffc6c 100644 --- a/__tests__/paddle.ts +++ b/__tests__/paddle.ts @@ -34,6 +34,7 @@ import { DEFAULT_CORES_METADATA, PricingType, } from '../src/common/paddle/pricing'; +import { CountryCode, CurrencyCode } from '@paddle/paddle-node-sdk'; let app: FastifyInstance; let con: DataSource; @@ -45,7 +46,7 @@ beforeAll(async () => { con = await createOrGetConnection(); app = await appFunc(); state = await initializeGraphQLTesting( - () => new MockContext(con, loggedUser), + () => new MockContext(con, loggedUser || undefined), ); client = state.client; return app.ready(); @@ -371,8 +372,8 @@ describe('plus pricing metadata', () => { describe('plus pricing preview', () => { const QUERY = /* GraphQL */ ` - query PricingPreview($type: PricingType) { - pricingPreview(type: $type) { + query PricingPreview($type: PricingType, $locale: String) { + pricingPreview(type: $type, locale: $locale) { metadata { appsId title @@ -562,6 +563,141 @@ describe('plus pricing preview', () => { expect(result).toEqual(result2); }); + + it('should format prices according to locale', async () => { + loggedUser = 'whp-1'; + const mockPreview = { + customerId: '1', + addressId: '1', + businessId: null, + discountId: null, + address: { + countryCode: 'DE' as CountryCode, + postalCode: '12345', + }, + customerIpAddress: '127.0.0.1', + availablePaymentMethods: ['card' as const], + details: { + lineItems: [ + { + price: { + id: 'pri_monthly', + productId: 'prod_123', + name: 'Monthly Subscription', + description: 'Monthly subscription for Daily.dev Plus', + type: 'standard' as const, + status: 'active' as const, + quantity: { + minimum: 1, + maximum: 1, + }, + customData: { + label: 'Monthly', + appsId: 'monthly-sub', + }, + billingCycle: { + interval: 'month' as const, + frequency: 1, + }, + trialPeriod: null, + unitPrice: { + amount: '5.00', + currencyCode: 'EUR' as CurrencyCode, + }, + unitPriceOverrides: [], + taxMode: 'internal' as const, + productTaxCategory: 'standard', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + importMeta: null, + product: { + id: 'prod_123', + name: 'Daily.dev Plus', + description: 'Daily.dev Plus Subscription', + type: 'standard' as const, + taxCategory: 'standard' as const, + imageUrl: 'https://daily.dev/plus.png', + customData: {}, + status: 'active' as const, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + importMeta: null, + prices: [], + }, + }, + product: { + id: 'prod_123', + name: 'Daily.dev Plus', + description: 'Daily.dev Plus Subscription', + type: 'standard' as const, + taxCategory: 'standard' as const, + imageUrl: 'https://daily.dev/plus.png', + customData: {}, + status: 'active' as const, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + importMeta: null, + prices: [], + }, + quantity: 1, + taxRate: '0.00', + unitTotals: { + subtotal: '5.00', + discount: '0.00', + tax: '0.00', + total: '5.00', + }, + formattedUnitTotals: { + subtotal: '€5,00', + discount: '€0,00', + tax: '€0,00', + total: '€5,00', + }, + formattedTotals: { + subtotal: '€5,00', + discount: '€0,00', + tax: '€0,00', + total: '€5,00', + }, + totals: { + subtotal: '5.00', + discount: '0.00', + tax: '0.00', + total: '5.00', + }, + discounts: [], + }, + ], + }, + currencyCode: 'EUR' as CurrencyCode, + }; + + jest + .spyOn(paddleInstance.pricingPreview, 'preview') + .mockResolvedValue(mockPreview as never); + + const result = await client.query(QUERY, { + variables: { + type: PricingType.Plus, + locale: 'de-DE', + }, + }); + expect(result.data.pricingPreview).toHaveLength(1); + const preview = result.data.pricingPreview[0]; + expect(preview.price.formatted).toBe('€5,00'); + }); + + it('should use default locale when not specified', async () => { + loggedUser = 'whp-1'; + const result = await client.query(QUERY, { + variables: { + type: PricingType.Plus, + }, + }); + expect(result.data.pricingPreview).toHaveLength(1); + const preview = result.data.pricingPreview[0]; + expect(preview.price.formatted).toBe('$5.00'); + }); }); describe('plus subscription', () => { diff --git a/src/schema/paddle.ts b/src/schema/paddle.ts index 937440b836..bf3fc95b68 100644 --- a/src/schema/paddle.ts +++ b/src/schema/paddle.ts @@ -131,7 +131,7 @@ export const typeDefs = /* GraphQL */ ` pricePreviews: PricePreviews! @auth corePricePreviews: PricePreviews! @auth pricingMetadata(type: PricingType): [ProductPricingMetadata!]! @auth - pricingPreview(type: PricingType): [ProductPricingPreview!]! @auth + pricingPreview(type: PricingType, locale: String): [ProductPricingPreview!]! } ${toGQLEnum(PricingType, 'PricingType')} @@ -275,6 +275,7 @@ export interface GQLCustomData { interface PaddlePricingPreviewArgs { type?: PricingType; + locale?: string; } export const resolvers: IResolvers = traceResolvers< @@ -389,7 +390,7 @@ export const resolvers: IResolvers = traceResolvers< ): Promise => getPricingMetadata(ctx, type), pricingPreview: async ( _, - { type = PricingType.Plus }: PaddlePricingPreviewArgs, + { type = PricingType.Plus, locale }: PaddlePricingPreviewArgs, ctx, ): Promise => { const metadata = await getPricingMetadata(ctx, type); @@ -416,7 +417,7 @@ export const resolvers: IResolvers = traceResolvers< return { metadata: meta, priceId: item.price.id, - price: getProductPrice(item), + price: getProductPrice(item, locale), currency: { code: preview.currencyCode, symbol: removeNumbers(item.formattedTotals.total),