Skip to content

Commit d131cf9

Browse files
author
Benjamin Piouffle
committed
decouple logic from taxes
1 parent 656cb1a commit d131cf9

File tree

7 files changed

+46
-69
lines changed

7 files changed

+46
-69
lines changed

server/graphql/v1/CollectiveInterface.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,15 +1358,12 @@ const CollectiveFields = () => {
13581358
where.slug = { [Op.in]: args.slugs };
13591359
}
13601360

1361+
// Filter out forbidden tier types
13611362
if (args.onlyValid !== false) {
13621363
const host = await req.loaders.Collective.host.load(collective);
1363-
if (Tier.hostHasDisabledTaxableTiers(host)) {
1364-
const validTypes = AllTierTypes.filter(
1365-
type => !models.Tier.isForbiddenTaxableTierType(collective, host, type),
1366-
);
1367-
if (validTypes.length !== AllTierTypes.length) {
1368-
where.type = validTypes;
1369-
}
1364+
const allowedTierTypes = Tier.getAllowedTierTypes(collective, host);
1365+
if (allowedTierTypes.length !== AllTierTypes.length) {
1366+
where.type = allowedTierTypes;
13701367
}
13711368
}
13721369

server/graphql/v2/interface/AccountWithContributions.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const AccountWithContributionsFields = {
6262
onlyValid: {
6363
type: GraphQLBoolean,
6464
description:
65-
'When true (default), exclude tiers with types that are no longer supported (e.g. taxable tiers when disabled by host). Use false for edit pages to show all tiers.',
65+
'When true (default), exclude tiers with types that are no longer supported (e.g. types in host.disabledTierTypes). Use false for edit pages to show all tiers.',
6666
defaultValue: true,
6767
},
6868
},
@@ -78,11 +78,9 @@ export const AccountWithContributionsFields = {
7878
const where: WhereOptions<Tier> = { CollectiveId: account.id };
7979
if (args.onlyValid !== false) {
8080
const host = await req.loaders.Collective.host.load(account);
81-
if (Tier.hostHasDisabledTaxableTiers(host)) {
82-
const validTypes = AllTierTypes.filter(type => !models.Tier.isForbiddenTaxableTierType(account, host, type));
83-
if (validTypes.length !== AllTierTypes.length) {
84-
where.type = validTypes;
85-
}
81+
const allowedTierTypes = Tier.getAllowedTierTypes(account, host);
82+
if (allowedTierTypes.length !== AllTierTypes.length) {
83+
where.type = allowedTierTypes;
8684
}
8785
}
8886

@@ -99,14 +97,10 @@ export const AccountWithContributionsFields = {
9997
supportedTierTypes: {
10098
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLTierType))),
10199
description:
102-
'Tier types that can be created for this account. Uses host tax settings and account overrides via Tier.isForbiddenTaxableTierType.',
100+
'Tier types that can be created for this account. Uses host disabledTierTypes and account overrides via Tier.isForbiddenTierType.',
103101
async resolve(account: Collective, _, req: express.Request): Promise<readonly TierType[]> {
104102
const host = await req.loaders.Collective.host.load(account);
105-
if (Tier.hostHasDisabledTaxableTiers(host)) {
106-
return AllTierTypes.filter(tierType => !models.Tier.isForbiddenTaxableTierType(account, host, tierType));
107-
} else {
108-
return AllTierTypes;
109-
}
103+
return Tier.getAllowedTierTypes(account, host).toSorted();
110104
},
111105
},
112106
contributors: {

server/graphql/v2/mutation/TierMutations.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { purgeCacheForCollective } from '../../../lib/cache';
55
import { purgeCacheForPage } from '../../../lib/cloudflare';
66
import { EntityShortIdPrefix, isEntityPublicId } from '../../../lib/permalink/entity-map';
77
import twoFactorAuthLib from '../../../lib/two-factor-authentication';
8-
import models, { Tier as TierModel } from '../../../models';
8+
import models, { Tier, Tier as TierModel } from '../../../models';
99
import { checkRemoteUserCanUseAccount } from '../../common/scope-check';
1010
import { NotFound, Unauthorized, ValidationFailed } from '../../errors';
1111
import { getIntervalFromTierFrequency } from '../enum/TierFrequency';
@@ -102,11 +102,11 @@ const tierMutations = {
102102
// Check 2FA
103103
await twoFactorAuthLib.enforceForAccount(req, collective, { onlyAskOnLogin: true });
104104

105-
// Validate taxable tier restriction when changing type
105+
// Validate tier type restriction when changing type
106106
const host = await req.loaders.Collective.host.load(collective);
107-
if (args.tier.type && models.Tier.isForbiddenTaxableTierType(collective, host, args.tier.type)) {
107+
if (args.tier.type && !Tier.getAllowedTierTypes(collective, host).includes(args.tier.type)) {
108108
throw new ValidationFailed(
109-
'This tier type is not allowed by your fiscal host, because it is subject to taxes in your country. Reach out to them for more information.',
109+
'This tier type is not allowed by your fiscal host. Reach out to them for more information.',
110110
);
111111
}
112112

@@ -143,11 +143,11 @@ const tierMutations = {
143143
// Check 2FA
144144
await twoFactorAuthLib.enforceForAccount(req, account, { onlyAskOnLogin: true });
145145

146-
// Validate taxable tier restriction
146+
// Validate tier type restriction
147147
const host = await req.loaders.Collective.host.load(account);
148-
if (models.Tier.isForbiddenTaxableTierType(account, host, args.tier.type)) {
148+
if (args.tier.type && !Tier.getAllowedTierTypes(account, host).includes(args.tier.type)) {
149149
throw new ValidationFailed(
150-
'This tier type is not allowed by your fiscal host, because it is subject to taxes in your country. Reach out to them for more information.',
150+
'This tier type is not allowed by your fiscal host. Reach out to them for more information.',
151151
);
152152
}
153153

server/lib/collectivelib.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { MODERATION_CATEGORIES } from '../constants/moderation-categories';
99
import PlatformConstants from '../constants/platform';
1010
import { VAT_OPTIONS } from '../constants/vat';
1111
import models, { Collective, Member, Op, sequelize, User } from '../models';
12+
import { AllTierTypes } from '../models/Tier';
1213

1314
import logger from './logger';
1415
import { stripHTML } from './sanitize-html';
@@ -177,7 +178,7 @@ export const COLLECTIVE_SETTINGS_KEYS_LIST = [
177178
'showSetupGuide',
178179
'showInitialOverviewSubscriptionCard',
179180
'kyc',
180-
'disableTaxableTiers',
181+
'disabledTierTypes',
181182
];
182183

183184
/**
@@ -199,8 +200,12 @@ export function filterCollectiveSettings(settings: Record<string, unknown> | nul
199200
preparedSettings.GST = pick(preparedSettings.GST, ['number', 'disabled']);
200201
}
201202

202-
if (preparedSettings.disableTaxableTiers !== undefined) {
203-
preparedSettings.disableTaxableTiers = Boolean(preparedSettings.disableTaxableTiers);
203+
if (preparedSettings.disabledTierTypes !== undefined) {
204+
preparedSettings.disabledTierTypes = Array.isArray(preparedSettings.disabledTierTypes)
205+
? preparedSettings.disabledTierTypes.filter(
206+
(t: unknown) => typeof t === 'string' && (AllTierTypes as readonly string[]).includes(t),
207+
)
208+
: [];
204209
}
205210

206211
// Generate warnings for invalid settings

server/models/Collective.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ type Settings = {
192192
applyMessage?: string;
193193
tos?: string;
194194
expenseTypes?: Partial<Record<ExpenseType, boolean>>;
195-
disableTaxableTiers?: boolean;
195+
disabledTierTypes?: string[];
196196
} & TaxSettings;
197197

198198
type Data = Partial<{

server/models/Tier.ts

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
import {
2-
accountHasGST,
3-
isMemberOfTheEuropeanUnion,
4-
isTierTypeSubjectToGST,
5-
isTierTypeSubjectToVAT,
6-
} from '@opencollective/taxes';
71
import debugLib from 'debug';
82
import slugify from 'limax';
93
import { defaults, get, isNil, min, uniq } from 'lodash';
@@ -188,30 +182,19 @@ class Tier extends ModelWithPublicId<EntityShortIdPrefix.Tier, InferAttributes<T
188182
});
189183
};
190184

191-
static hostHasDisabledTaxableTiers = (host: Collective): boolean => {
192-
return Boolean(host?.settings?.disableTaxableTiers);
193-
};
194-
195-
static isForbiddenTaxableTierType = (account: Collective, host: Collective, tierType: TierType): boolean => {
196-
// Check if taxable tiers are disabled at the host level
197-
if (!Tier.hostHasDisabledTaxableTiers(host)) {
198-
return false;
199-
}
200-
201-
// Check if taxable tiers are allowed at the account level (override)
202-
const allowTaxableTiers = get(account, 'data.allowTaxableTiers');
203-
if (allowTaxableTiers === true || (Array.isArray(allowTaxableTiers) && allowTaxableTiers.includes(tierType))) {
204-
return false;
185+
static getAllowedTierTypes = (account: Collective, host: Collective): readonly TierType[] => {
186+
const disabledTypes = host?.settings?.disabledTierTypes;
187+
if (!Array.isArray(disabledTypes) || !disabledTypes?.length) {
188+
return AllTierTypes;
205189
}
206190

207-
// Check if the tier type is subject to VAT or GST
208-
if (isMemberOfTheEuropeanUnion(host.countryISO)) {
209-
return isTierTypeSubjectToVAT(tierType);
210-
} else if (host.countryISO === 'NZ' || accountHasGST(account)) {
211-
return isTierTypeSubjectToGST(tierType);
191+
const overrideAllowedTierTypesSettings = get(account, 'data.allowedTierTypes');
192+
let overrideAllowedTierTypes: string[] = [];
193+
if (overrideAllowedTierTypesSettings && Array.isArray(overrideAllowedTierTypesSettings)) {
194+
overrideAllowedTierTypes = overrideAllowedTierTypesSettings;
212195
}
213196

214-
return false;
197+
return AllTierTypes.filter(type => !disabledTypes.includes(type) || overrideAllowedTierTypes.includes(type));
215198
};
216199

217200
/**

test/server/graphql/v2/mutation/TierMutations.test.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,14 @@ describe('server/graphql/v2/mutation/TierMutations', () => {
134134
expect(createdTier.currency).to.eql('EUR');
135135
});
136136

137-
it('rejects taxable tier types when host has disableTaxableTiers', async () => {
138-
const host = await fakeCollective({ admin: adminUser, countryISO: 'FR' });
137+
it('rejects tier types when host has disabledTierTypes', async () => {
138+
const host = await fakeCollective({ admin: adminUser });
139139
const hostedCollective = await fakeCollective({
140140
admin: adminUser,
141141
HostCollectiveId: host.id,
142142
settings: {},
143143
});
144-
await host.update({ settings: { ...host.settings, disableTaxableTiers: true } });
145-
await fakeMember({ CollectiveId: adminUser.id, MemberCollectiveId: hostedCollective.id, role: roles.ADMIN });
144+
await host.update({ settings: { ...host.settings, disabledTierTypes: ['PRODUCT', 'SERVICE', 'TICKET'] } });
146145

147146
const result = await graphqlQueryV2(
148147
CREATE_TIER_MUTATION,
@@ -162,15 +161,14 @@ describe('server/graphql/v2/mutation/TierMutations', () => {
162161
expect(result.errors[0].message).to.include('not allowed');
163162
});
164163

165-
it('allows taxable tier types when collective has allowTaxableTiers override in data', async () => {
166-
const host = await fakeCollective({ admin: adminUser, countryISO: 'FR' });
164+
it('allows tier types when collective has allowedTierTypes override in data', async () => {
165+
const host = await fakeCollective({ admin: adminUser });
167166
const hostedCollective = await fakeCollective({
168167
admin: adminUser,
169168
HostCollectiveId: host.id,
170169
});
171-
await hostedCollective.update({ data: { ...hostedCollective.data, allowTaxableTiers: true } });
172-
await host.update({ settings: { ...host.settings, disableTaxableTiers: true } });
173-
await fakeMember({ CollectiveId: adminUser.id, MemberCollectiveId: hostedCollective.id, role: roles.ADMIN });
170+
await hostedCollective.update({ data: { ...hostedCollective.data, allowedTierTypes: ['PRODUCT'] } });
171+
await host.update({ settings: { ...host.settings, disabledTierTypes: ['PRODUCT', 'SERVICE', 'TICKET'] } });
174172

175173
const result = await graphqlQueryV2(
176174
CREATE_TIER_MUTATION,
@@ -186,6 +184,7 @@ describe('server/graphql/v2/mutation/TierMutations', () => {
186184
},
187185
adminUser,
188186
);
187+
result.errors && console.error(result.errors);
189188
expect(result.errors).to.not.exist;
190189
expect(result.data.createTier.legacyId).to.exist;
191190
});
@@ -295,14 +294,13 @@ describe('server/graphql/v2/mutation/TierMutations', () => {
295294
expect(editedTier.interval).to.equal(existingTier.interval);
296295
});
297296

298-
it('rejects changing tier type to taxable when host has disableTaxableTiers', async () => {
299-
const host = await fakeCollective({ admin: adminUser, countryISO: 'FR' });
297+
it('rejects changing tier type when host has disabledTierTypes', async () => {
298+
const host = await fakeCollective({ admin: adminUser });
300299
const hostedCollective = await fakeCollective({
301300
admin: adminUser,
302301
HostCollectiveId: host.id,
303302
});
304-
await host.update({ settings: { ...host.settings, disableTaxableTiers: true } });
305-
await fakeMember({ CollectiveId: adminUser.id, MemberCollectiveId: hostedCollective.id, role: roles.ADMIN });
303+
await host.update({ settings: { ...host.settings, disabledTierTypes: ['PRODUCT', 'SERVICE', 'TICKET'] } });
306304

307305
const tier = await fakeTier({
308306
CollectiveId: hostedCollective.id,

0 commit comments

Comments
 (0)