Skip to content

Commit

Permalink
feat(core): Add currencyCode to variant price model
Browse files Browse the repository at this point in the history
Relates to #1691
This change puts in place a data model which will allow us to support multiple currencies per
channel, and will remove the requirement that a new price be set for
each channel that a variant is assigned to.

BREAKING CHANGE: The `Channel.currencyCode` field has been renamed to `defaultCurrencyCode`, and a
new `currencyCode` field has been added to the `ProductVariantPrice` entity. This will require
a database migration with care taken to preserve exiting data.
  • Loading branch information
michaelbromley committed Feb 22, 2023
1 parent c98ba2f commit 24e558b
Show file tree
Hide file tree
Showing 16 changed files with 43 additions and 16 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/api/common/request-context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('RequestContext', () => {
token: 'oiajwodij09au3r',
id: '995859',
code: '__default_channel__',
currencyCode: CurrencyCode.EUR,
defaultCurrencyCode: CurrencyCode.EUR,
pricesIncludeTax: true,
defaultLanguageCode: LanguageCode.en,
defaultShippingZone: zone,
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/api/common/request-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LanguageCode, Permission } from '@vendure/common/lib/generated-types';
import { CurrencyCode, LanguageCode, Permission } from '@vendure/common/lib/generated-types';
import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
import { isObject } from '@vendure/common/lib/shared-utils';
import { Request } from 'express';
Expand Down Expand Up @@ -43,6 +43,7 @@ export type SerializedRequestContext = {
*/
export class RequestContext {
private readonly _languageCode: LanguageCode;
private readonly _currencyCode: CurrencyCode;
private readonly _channel: Channel;
private readonly _session?: CachedSession;
private readonly _isAuthorized: boolean;
Expand All @@ -60,16 +61,18 @@ export class RequestContext {
channel: Channel;
session?: CachedSession;
languageCode?: LanguageCode;
currencyCode?: CurrencyCode;
isAuthorized: boolean;
authorizedAsOwnerOnly: boolean;
translationFn?: TFunction;
}) {
const { req, apiType, channel, session, languageCode, translationFn } = options;
const { req, apiType, channel, session, languageCode, currencyCode, translationFn } = options;
this._req = req;
this._apiType = apiType;
this._channel = channel;
this._session = session;
this._languageCode = languageCode || (channel && channel.defaultLanguageCode);
this._currencyCode = currencyCode || (channel && channel.defaultCurrencyCode);
this._isAuthorized = options.isAuthorized;
this._authorizedAsOwnerOnly = options.authorizedAsOwnerOnly;
this._translationFn = translationFn || (((key: string) => key) as any);
Expand Down Expand Up @@ -185,6 +188,10 @@ export class RequestContext {
return this._languageCode;
}

get currencyCode(): CurrencyCode {
return this._currencyCode;
}

get session(): CachedSession | undefined {
return this._session;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ export class ChannelEntityResolver {
? channel.seller ?? (await this.sellerService.findOne(ctx, channel.sellerId))
: undefined;
}

@ResolveField()
currencyCode(@Ctx() ctx: RequestContext, @Parent() channel: Channel): string {
return channel.defaultCurrencyCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ProductVariantPriceCalculationStrategy extends InjectableStrate

/**
* @description
* The arguments passed the the `calculate` method of the configured {@link ProductVariantPriceCalculationStrategy}.
* The arguments passed the `calculate` method of the configured {@link ProductVariantPriceCalculationStrategy}.
*
* @docsCategory configuration
* @docsPage ProductVariantPriceCalculationStrategy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class FastImporterService {
const variantPrice = new ProductVariantPrice({
price: input.price,
channelId,
currencyCode: this.defaultChannel.defaultCurrencyCode,
});
variantPrice.variant = createdVariant;
await this.connection
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/entity/channel/channel.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class Channel extends VendureEntity {
defaultShippingZone: Zone;

@Column('varchar')
currencyCode: CurrencyCode;
defaultCurrencyCode: CurrencyCode;

@Column(type => CustomChannelFields)
customFields: CustomChannelFields;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CurrencyCode } from '@vendure/common/lib/generated-types';
import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
import { Column, Entity, Index, ManyToOne } from 'typeorm';

Expand All @@ -22,9 +23,12 @@ export class ProductVariantPrice extends VendureEntity {

@Money() price: number;

@EntityId() channelId: ID;
@EntityId({ nullable: true }) channelId: ID;

@Column('varchar')
currencyCode: CurrencyCode;

@Index()
@ManyToOne(type => ProductVariant, variant => variant.productVariantPrices)
@ManyToOne(type => ProductVariant, variant => variant.productVariantPrices, { onDelete: 'CASCADE' })
variant: ProductVariant;
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class MysqlSearchStrategy implements SearchStrategy {
.limit(take)
.offset(skip)
.getRawMany()
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.defaultCurrencyCode)));
}

async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class PostgresSearchStrategy implements SearchStrategy {
.limit(take)
.offset(skip)
.getRawMany()
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.defaultCurrencyCode)));
}

async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class SqliteSearchStrategy implements SearchStrategy {
.limit(take)
.offset(skip)
.getRawMany()
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.defaultCurrencyCode)));
}

async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class ProductPriceApplicator {
variant.listPrice = price;
variant.listPriceIncludesTax = priceIncludesTax;
variant.taxRateApplied = applicableTaxRate;
variant.currencyCode = ctx.channel.currencyCode;
variant.currencyCode = channelPrice.currencyCode;
return variant;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { LanguageCode, Permission } from '@vendure/common/lib/generated-types';
import { CurrencyCode, LanguageCode, Permission } from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';
import { Request } from 'express';
import { GraphQLResolveInfo } from 'graphql';
Expand Down Expand Up @@ -97,6 +97,7 @@ export class RequestContextService {

const hasOwnerPermission = !!requiredPermissions && requiredPermissions.includes(Permission.Owner);
const languageCode = this.getLanguageCode(req, channel);
const currencyCode = this.getCurrencyCode(req, channel);
const user = session && session.user;
const isAuthorized = this.userHasRequiredPermissionsOnChannel(requiredPermissions, channel, user);
const authorizedAsOwnerOnly = !isAuthorized && hasOwnerPermission;
Expand All @@ -106,6 +107,7 @@ export class RequestContextService {
apiType,
channel,
languageCode,
currencyCode,
session,
isAuthorized,
authorizedAsOwnerOnly,
Expand Down Expand Up @@ -133,6 +135,10 @@ export class RequestContextService {
);
}

private getCurrencyCode(req: Request, channel: Channel): CurrencyCode | undefined {
return (req.query && (req.query.currencyCode as CurrencyCode)) ?? channel.defaultCurrencyCode;
}

/**
* TODO: Deprecate and remove, since this function is now handled internally in the RequestContext.
* @private
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/service/services/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,10 @@ export class ChannelService {
ctx: RequestContext,
input: CreateChannelInput,
): Promise<ErrorResultUnion<CreateChannelResult, Channel>> {
const channel = new Channel(input);
const channel = new Channel({
...input,
defaultCurrencyCode: input.currencyCode,
});
const defaultLanguageValidationResult = await this.validateDefaultLanguageCode(ctx, input);
if (isGraphQlErrorResult(defaultLanguageValidationResult)) {
return defaultLanguageValidationResult;
Expand Down Expand Up @@ -347,7 +350,7 @@ export class ChannelService {
code: DEFAULT_CHANNEL_CODE,
defaultLanguageCode: this.configService.defaultLanguageCode,
pricesIncludeTax: false,
currencyCode: CurrencyCode.USD,
defaultCurrencyCode: CurrencyCode.USD,
token: defaultChannelToken,
});
} else if (defaultChannelToken && defaultChannel.token !== defaultChannelToken) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/service/services/order.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export class OrderService {
billingAddress: {},
subTotal: 0,
subTotalWithTax: 0,
currencyCode: ctx.channel.currencyCode,
currencyCode: ctx.currencyCode,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ export class ProductVariantService {
variantPrice = new ProductVariantPrice({
channelId,
variant: new ProductVariant({ id: productVariantId }),
currencyCode: ctx.currencyCode,
});
}
variantPrice.price = price;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
productVariantPreviewFocalPoint: undefined,
price: 0,
priceWithTax: 0,
currencyCode: ctx.channel.currencyCode,
currencyCode: ctx.currencyCode,
description: productTranslation.description,
facetIds: product.facetValues?.map(fv => fv.facet.id.toString()) ?? [],
channelIds: [ctx.channelId],
Expand Down

0 comments on commit 24e558b

Please sign in to comment.