diff --git a/license/signatures/version1/cla.json b/license/signatures/version1/cla.json index 0d5409580c..f8f21b3f32 100644 --- a/license/signatures/version1/cla.json +++ b/license/signatures/version1/cla.json @@ -279,6 +279,22 @@ "created_at": "2024-10-31T08:42:52Z", "repoId": 136938012, "pullRequestNo": 3174 + }, + { + "name": "kkerti", + "id": 47832952, + "comment_id": 2458191015, + "created_at": "2024-11-05T21:33:05Z", + "repoId": 136938012, + "pullRequestNo": 3187 + }, + { + "name": "shingoaoyama1", + "id": 17615101, + "comment_id": 2459213307, + "created_at": "2024-11-06T10:15:37Z", + "repoId": 136938012, + "pullRequestNo": 3192 } ] -} +} \ No newline at end of file diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index 66cab32f58..02a8d8990a 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -87,6 +87,7 @@ export * from './system/health-check-strategy'; export * from './system/error-handler-strategy'; export * from './tax/default-tax-line-calculation-strategy'; export * from './tax/default-tax-zone-strategy'; +export * from './tax/address-based-tax-zone-strategy'; export * from './tax/tax-line-calculation-strategy'; export * from './tax/tax-zone-strategy'; export * from './vendure-config'; diff --git a/packages/core/src/config/tax/address-based-tax-zone-strategy.spec.ts b/packages/core/src/config/tax/address-based-tax-zone-strategy.spec.ts new file mode 100644 index 0000000000..9aff9f28ee --- /dev/null +++ b/packages/core/src/config/tax/address-based-tax-zone-strategy.spec.ts @@ -0,0 +1,52 @@ +import { RequestContext, Zone, Channel, Order } from '@vendure/core'; +import { describe, it, expect } from 'vitest'; + +import { Logger } from '../logger/vendure-logger'; + +import { AddressBasedTaxZoneStrategy } from './address-based-tax-zone-strategy'; + +describe('AddressBasedTaxZoneStrategy', () => { + const strategy = new AddressBasedTaxZoneStrategy(); + + const ctx = {} as RequestContext; + const defaultZone = { name: 'Default Zone' } as Zone; + const channel = { defaultTaxZone: defaultZone } as Channel; + + const zones: Zone[] = [ + defaultZone, + { name: 'US Zone', members: [{ code: 'US' }] } as Zone, + { name: 'DE Zone', members: [{ code: 'DE' }] } as Zone, + ]; + + it('Determines zone based on billing address country', () => { + const order = { + billingAddress: { countryCode: 'US' }, + shippingAddress: { countryCode: 'DE' }, + } as Order; + const result = strategy.determineTaxZone(ctx, zones, channel, order); + expect(result.name).toBe('US Zone'); + }); + + it('Determines zone based on shipping address if no billing address set', () => { + const order = { shippingAddress: { countryCode: 'DE' } } as Order; + const result = strategy.determineTaxZone(ctx, zones, channel, order); + expect(result.name).toBe('DE Zone'); + }); + + it('Returns the default zone if no matching zone is found', () => { + const order = { billingAddress: { countryCode: 'FR' } } as Order; + const result = strategy.determineTaxZone(ctx, zones, channel, order); + expect(result.name).toBe('Default Zone'); + }); + + it('Returns the default zone if no order is provided', () => { + const result = strategy.determineTaxZone(ctx, zones, channel); + expect(result.name).toBe('Default Zone'); + }); + + it('Returns the default zone if no address is set on order', () => { + const order = {} as Order; + const result = strategy.determineTaxZone(ctx, zones, channel, order); + expect(result.name).toBe('Default Zone'); + }); +}); diff --git a/packages/core/src/config/tax/address-based-tax-zone-strategy.ts b/packages/core/src/config/tax/address-based-tax-zone-strategy.ts new file mode 100644 index 0000000000..ae53413505 --- /dev/null +++ b/packages/core/src/config/tax/address-based-tax-zone-strategy.ts @@ -0,0 +1,42 @@ +import { RequestContext } from '../../api/common/request-context'; +import { Channel, Order, Zone } from '../../entity'; +import { Logger } from '../logger/vendure-logger'; + +import { TaxZoneStrategy } from './tax-zone-strategy'; + +const loggerCtx = 'AddressBasedTaxZoneStrategy'; + +/** + * @description + * Address based {@link TaxZoneStrategy} which tries to find the applicable {@link Zone} based on the + * country of the billing address, or else the country of the shipping address of the Order. + * + * Returns the default {@link Channel}'s default tax zone if no applicable zone is found. + * + * @since 3.1.0 + * + * :::info + * + * This is configured via `taxOptions.taxZoneStrategy = new AddressBasedTaxZoneStrategy()` in + * your VendureConfig. + * + * ::: + * + * @docsCategory tax + */ +export class AddressBasedTaxZoneStrategy implements TaxZoneStrategy { + determineTaxZone(ctx: RequestContext, zones: Zone[], channel: Channel, order?: Order): Zone { + const countryCode = order?.billingAddress?.countryCode ?? order?.shippingAddress?.countryCode; + if (order && countryCode) { + const zone = zones.find(z => z.members?.find(member => member.code === countryCode)); + if (zone) { + return zone; + } + Logger.debug( + `No tax zone found for country ${countryCode}. Returning default ${channel.defaultTaxZone.name} for order ${order.code}`, + loggerCtx, + ); + } + return channel.defaultTaxZone; + } +}