From bb41c0b1ab2e8182ca34976fdee687cead90eebb Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 23 Apr 2025 01:59:24 +0600 Subject: [PATCH 1/6] [FSSDK-11399] support traffic allocation for cmab --- lib/core/decision_service/index.spec.ts | 132 +++++++++++++++++++++- lib/core/decision_service/index.ts | 33 +++++- lib/project_config/project_config.spec.ts | 30 +---- lib/project_config/project_config.ts | 16 --- lib/shared_types.ts | 1 + vitest.config.mts | 2 +- 6 files changed, 166 insertions(+), 48 deletions(-) diff --git a/lib/core/decision_service/index.spec.ts b/lib/core/decision_service/index.spec.ts index e2a186eca..8ddc736eb 100644 --- a/lib/core/decision_service/index.spec.ts +++ b/lib/core/decision_service/index.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { describe, it, expect, vi, MockInstance, beforeEach } from 'vitest'; -import { CMAB_FETCH_FAILED, DecisionService } from '.'; +import { CMAB_DUMMY_ENTITY_ID, CMAB_FETCH_FAILED, DecisionService } from '.'; import { getMockLogger } from '../../tests/mock/mock_logger'; import OptimizelyUserContext from '../../optimizely_user_context'; import { bucket } from '../bucketer'; @@ -140,10 +140,18 @@ const verifyBucketCall = ( variationIdMap, bucketingId, } = mockBucket.mock.calls[call][0]; + let expectedTrafficAllocation = experiment.trafficAllocation; + if (experiment.cmab) { + expectedTrafficAllocation = [{ + endOfRange: experiment.cmab.trafficAllocation, + entityId: CMAB_DUMMY_ENTITY_ID, + }]; + } + expect(experimentId).toBe(experiment.id); expect(experimentKey).toBe(experiment.key); expect(userId).toBe(user.getUserId()); - expect(trafficAllocationConfig).toBe(experiment.trafficAllocation); + expect(trafficAllocationConfig).toEqual(expectedTrafficAllocation); expect(experimentKeyMap).toBe(projectConfig.experimentKeyMap); expect(experimentIdMap).toBe(projectConfig.experimentIdMap); expect(groupIdMap).toBe(projectConfig.groupIdMap); @@ -1327,7 +1335,8 @@ describe('DecisionService', () => { }); }); - it('should get decision from the cmab service if the experiment is a cmab experiment', async () => { + it('should not return variation and should not call cmab service \ + for cmab experiment if user is not bucketed into it', async () => { const { decisionService, cmabService } = getDecisionService(); const config = createProjectConfig(getDecisionTestDatafile()); @@ -1340,6 +1349,57 @@ describe('DecisionService', () => { }, }); + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'default-rollout-key') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + + const feature = config.featureKeyMap['flag_1']; + const value = decisionService.resolveVariationsForFeatureList('async', config, [feature], user, {}).get(); + expect(value).toBeInstanceOf(Promise); + + const variation = (await value)[0]; + expect(variation.result).toEqual({ + experiment: config.experimentKeyMap['default-rollout-key'], + variation: config.variationIdMap['5007'], + decisionSource: DECISION_SOURCES.ROLLOUT, + }); + + verifyBucketCall(0, config, config.experimentKeyMap['exp_3'], user); + expect(cmabService.getDecision).not.toHaveBeenCalled(); + }); + + it('should get decision from the cmab service if the experiment is a cmab experiment \ + and user is bucketed into it', async () => { + const { decisionService, cmabService } = getDecisionService(); + const config = createProjectConfig(getDecisionTestDatafile()); + + const user = new OptimizelyUserContext({ + optimizely: {} as any, + userId: 'tester', + attributes: { + country: 'BD', + age: 80, // should satisfy audience condition for exp_3 which is cmab and not others + }, + }); + + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + cmabService.getDecision.mockResolvedValue({ variationId: '5003', cmabUuid: 'uuid-test', @@ -1357,6 +1417,8 @@ describe('DecisionService', () => { decisionSource: DECISION_SOURCES.FEATURE_TEST, }); + verifyBucketCall(0, config, config.experimentKeyMap['exp_3'], user); + expect(cmabService.getDecision).toHaveBeenCalledTimes(1); expect(cmabService.getDecision).toHaveBeenCalledWith( config, @@ -1379,6 +1441,17 @@ describe('DecisionService', () => { }, }); + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + cmabService.getDecision.mockResolvedValue({ variationId: '5003', cmabUuid: 'uuid-test', @@ -1424,6 +1497,17 @@ describe('DecisionService', () => { }, }); + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + cmabService.getDecision.mockRejectedValue(new Error('I am an error')); const feature = config.featureKeyMap['flag_1']; @@ -1474,6 +1558,17 @@ describe('DecisionService', () => { userProfileServiceAsync?.save.mockImplementation(() => Promise.resolve()); + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + cmabService.getDecision.mockResolvedValue({ variationId: '5003', cmabUuid: 'uuid-test', @@ -1552,6 +1647,17 @@ describe('DecisionService', () => { userProfileServiceAsync?.save.mockImplementation(() => Promise.resolve()); + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); + cmabService.getDecision.mockResolvedValue({ variationId: '5003', cmabUuid: 'uuid-test', @@ -1605,6 +1711,16 @@ describe('DecisionService', () => { userProfileServiceAsync?.save.mockRejectedValue(new Error('I am an error')); + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); cmabService.getDecision.mockResolvedValue({ variationId: '5003', @@ -1669,6 +1785,16 @@ describe('DecisionService', () => { userProfileServiceAsync?.lookup.mockResolvedValue(null); userProfileServiceAsync?.save.mockResolvedValue(null); + mockBucket.mockImplementation((param: BucketerParams) => { + const ruleKey = param.experimentKey; + if (ruleKey == 'exp_3') { + return { result: param.trafficAllocationConfig[0].entityId, reasons: [] } + } + return { + result: null, + reasons: [], + } + }); cmabService.getDecision.mockResolvedValue({ variationId: '5003', diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index 82b6aa028..b542379d5 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -27,7 +27,6 @@ import { getExperimentFromId, getExperimentFromKey, getFlagVariationByKey, - getTrafficAllocation, getVariationIdFromExperimentAndVariationKey, getVariationFromId, getVariationKeyFromId, @@ -44,6 +43,7 @@ import { FeatureFlag, OptimizelyDecideOption, OptimizelyUserContext, + TrafficAllocation, UserAttributes, UserProfile, UserProfileService, @@ -148,6 +148,9 @@ type VariationIdWithCmabParams = { cmabUuid?: string; }; export type DecideOptionsMap = Partial>; + +export const CMAB_DUMMY_ENTITY_ID= '$' + /** * Optimizely's decision service that determines which variation of an experiment the user will be allocated to. * @@ -355,6 +358,24 @@ export class DecisionService { reasons: [[CMAB_NOT_SUPPORTED_IN_SYNC]], }); } + + const userId = user.getUserId(); + const attributes = user.getAttributes(); + + // by default, the bucketing ID should be the user ID + const bucketingId = this.getBucketingId(userId, attributes); + const bucketerParams = this.buildBucketerParams(configObj, experiment, bucketingId, userId); + + const bucketerResult = bucket(bucketerParams); + + // this means the user is not in the cmab experiment + if (bucketerResult.result !== CMAB_DUMMY_ENTITY_ID) { + return Value.of(op, { + error: false, + result: {}, + reasons: bucketerResult.reasons, + }); + } const cmabPromise = this.cmabService.getDecision(configObj, user, experiment.id, decideOptions).then( (cmabDecision) => { @@ -573,6 +594,14 @@ export class DecisionService { bucketingId: string, userId: string ): BucketerParams { + let trafficAllocationConfig: TrafficAllocation[] = experiment.trafficAllocation; + if (experiment.cmab) { + trafficAllocationConfig = [{ + entityId: CMAB_DUMMY_ENTITY_ID, + endOfRange: experiment.cmab.trafficAllocation + }]; + } + return { bucketingId, experimentId: experiment.id, @@ -581,7 +610,7 @@ export class DecisionService { experimentKeyMap: configObj.experimentKeyMap, groupIdMap: configObj.groupIdMap, logger: this.logger, - trafficAllocationConfig: getTrafficAllocation(configObj, experiment.id), + trafficAllocationConfig, userId, variationIdMap: configObj.variationIdMap, } diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 54aa75d97..4bbe7b3b7 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -250,16 +250,19 @@ describe('createProjectConfig - cmab experiments', () => { const datafile = testDatafile.getTestProjectConfig(); datafile.experiments[0].cmab = { attributes: ['808797688', '808797689'], + trafficAllocation: 3141, }; datafile.experiments[2].cmab = { attributes: ['808797689'], + trafficAllocation: 1414, }; const configObj = projectConfig.createProjectConfig(datafile); const experiment0 = configObj.experiments[0]; expect(experiment0.cmab).toEqual({ + trafficAllocation: 3141, attributeIds: ['808797688', '808797689'], }); @@ -268,6 +271,7 @@ describe('createProjectConfig - cmab experiments', () => { const experiment2 = configObj.experiments[2]; expect(experiment2.cmab).toEqual({ + trafficAllocation: 1414, attributeIds: ['808797689'], }); }); @@ -453,32 +457,6 @@ describe('getVariationKeyFromId', () => { }); }); -describe('getTrafficAllocation', () => { - let testData: Record; - let configObj: ProjectConfig; - - beforeEach(function() { - testData = cloneDeep(testDatafile.getTestProjectConfig()); - configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); - }); - - it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { - expect(projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id)).toEqual( - testData.experiments[0].trafficAllocation - ); - }); - - it('should throw error for invalid experient key in getTrafficAllocation', function() { - expect(() => { - projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); - }).toThrowError( - expect.objectContaining({ - baseMessage: INVALID_EXPERIMENT_ID, - params: ['invalidExperimentId'], - }) - ); - }); -}); describe('getVariationIdFromExperimentAndVariationKey', () => { let testData: Record; diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts index 5a7674668..9e1767f84 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -568,21 +568,6 @@ export const getExperimentFromKey = function(projectConfig: ProjectConfig, exper throw new OptimizelyError(EXPERIMENT_KEY_NOT_IN_DATAFILE, experimentKey); }; -/** - * Given an experiment id, returns the traffic allocation within that experiment - * @param {ProjectConfig} projectConfig Object representing project configuration - * @param {string} experimentId Id representing the experiment - * @return {TrafficAllocation[]} Traffic allocation for the experiment - * @throws If experiment key is not in datafile - */ -export const getTrafficAllocation = function(projectConfig: ProjectConfig, experimentId: string): TrafficAllocation[] { - const experiment = projectConfig.experimentIdMap[experimentId]; - if (!experiment) { - throw new OptimizelyError(INVALID_EXPERIMENT_ID, experimentId); - } - return experiment.trafficAllocation; -}; - /** * Get experiment from provided experiment id. Log an error if no experiment * exists in the project config with the given ID. @@ -890,7 +875,6 @@ export default { getVariationKeyFromId, getVariationIdFromExperimentAndVariationKey, getExperimentFromKey, - getTrafficAllocation, getExperimentFromId, getFlagVariationByKey, getFeatureFromKey, diff --git a/lib/shared_types.ts b/lib/shared_types.ts index ea15b21e3..c203613a3 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -159,6 +159,7 @@ export interface Experiment { forcedVariations?: { [key: string]: string }; isRollout?: boolean; cmab?: { + trafficAllocation: number; attributeIds: string[]; }; } diff --git a/vitest.config.mts b/vitest.config.mts index 584eeb60d..c35f62ec3 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -26,7 +26,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/*.spec.ts'], + include: ['**/decision_service/index.spec.ts'], typecheck: { enabled: true, tsconfig: 'tsconfig.spec.json', From 76740ed1528232358484b137bb1969953dea2aaa Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 23 Apr 2025 02:03:33 +0600 Subject: [PATCH 2/6] up --- lib/core/decision_service/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index b542379d5..417dc16f4 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -362,7 +362,6 @@ export class DecisionService { const userId = user.getUserId(); const attributes = user.getAttributes(); - // by default, the bucketing ID should be the user ID const bucketingId = this.getBucketingId(userId, attributes); const bucketerParams = this.buildBucketerParams(configObj, experiment, bucketingId, userId); From 6b4b722afb7791ce0350fcdfb216606287cca46c Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 23 Apr 2025 02:05:01 +0600 Subject: [PATCH 3/6] upd --- vitest.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.config.mts b/vitest.config.mts index c35f62ec3..584eeb60d 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -26,7 +26,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/decision_service/index.spec.ts'], + include: ['**/*.spec.ts'], typecheck: { enabled: true, tsconfig: 'tsconfig.spec.json', From d8b4f19b1cff378107d65bd747c85f95fd81d8cf Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 23 Apr 2025 02:06:30 +0600 Subject: [PATCH 4/6] fix --- lib/project_config/project_config.tests.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/project_config/project_config.tests.js b/lib/project_config/project_config.tests.js index 6e93327cc..612e93a12 100644 --- a/lib/project_config/project_config.tests.js +++ b/lib/project_config/project_config.tests.js @@ -402,21 +402,6 @@ describe('lib/core/project_config', function() { ); }); - it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { - assert.deepEqual( - projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id), - testData.experiments[0].trafficAllocation - ); - }); - - it('should throw error for invalid experient key in getTrafficAllocation', function() { - const ex = assert.throws(function() { - projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); - }); - assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); - assert.deepEqual(ex.params, ['invalidExperimentId']); - }); - describe('#getVariationIdFromExperimentAndVariationKey', function() { it('should return the variation id for the given experiment key and variation key', function() { assert.strictEqual( From 59a87eb57a8a4e328a6ec42c641ca25f9b5f832d Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 24 Apr 2025 00:21:20 +0600 Subject: [PATCH 5/6] attributeIds update --- lib/project_config/project_config.spec.ts | 4 ++-- lib/project_config/project_config.ts | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 4bbe7b3b7..2603d3af0 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -249,12 +249,12 @@ describe('createProjectConfig - cmab experiments', () => { it('should populate cmab field correctly', function() { const datafile = testDatafile.getTestProjectConfig(); datafile.experiments[0].cmab = { - attributes: ['808797688', '808797689'], + attributeIds: ['808797688', '808797689'], trafficAllocation: 3141, }; datafile.experiments[2].cmab = { - attributes: ['808797689'], + attributeIds: ['808797689'], trafficAllocation: 1414, }; diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts index 9e1767f84..14b9613fd 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -157,15 +157,6 @@ export const createProjectConfig = function(datafileObj?: JSON, datafileStr: str projectConfig.__datafileStr = datafileStr === null ? JSON.stringify(datafileObj) : datafileStr; - /** rename cmab.attributes field from the datafile to cmab.attributeIds for each experiment */ - projectConfig.experiments.forEach(experiment => { - if (experiment.cmab) { - const attributes = (experiment.cmab as any).attributes; - delete (experiment.cmab as any).attributes; - experiment.cmab.attributeIds = attributes; - } - }); - /* * Conditions of audiences in projectConfig.typedAudiences are not * expected to be string-encoded as they are here in projectConfig.audiences. From 761180ad794f0bd42a89bd8f4051d25228384499 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 24 Apr 2025 16:29:16 +0600 Subject: [PATCH 6/6] fix fsc --- lib/core/decision_service/index.ts | 3 ++- lib/project_config/project_config.spec.ts | 26 ++++++++++++++++++++++ lib/project_config/project_config.tests.js | 15 +++++++++++++ lib/project_config/project_config.ts | 17 ++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/core/decision_service/index.ts b/lib/core/decision_service/index.ts index 417dc16f4..5d5e57da9 100644 --- a/lib/core/decision_service/index.ts +++ b/lib/core/decision_service/index.ts @@ -32,6 +32,7 @@ import { getVariationKeyFromId, isActive, ProjectConfig, + getTrafficAllocation, } from '../../project_config/project_config'; import { AudienceEvaluator, createAudienceEvaluator } from '../audience_evaluator'; import * as stringValidator from '../../utils/string_value_validator'; @@ -593,7 +594,7 @@ export class DecisionService { bucketingId: string, userId: string ): BucketerParams { - let trafficAllocationConfig: TrafficAllocation[] = experiment.trafficAllocation; + let trafficAllocationConfig: TrafficAllocation[] = getTrafficAllocation(configObj, experiment.id); if (experiment.cmab) { trafficAllocationConfig = [{ entityId: CMAB_DUMMY_ENTITY_ID, diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 2603d3af0..5a0259ee4 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -457,6 +457,32 @@ describe('getVariationKeyFromId', () => { }); }); +describe('getTrafficAllocation', () => { + let testData: Record; + let configObj: ProjectConfig; + + beforeEach(function() { + testData = cloneDeep(testDatafile.getTestProjectConfig()); + configObj = projectConfig.createProjectConfig(cloneDeep(testData) as JSON); + }); + + it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { + expect(projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id)).toEqual( + testData.experiments[0].trafficAllocation + ); + }); + + it('should throw error for invalid experient key in getTrafficAllocation', function() { + expect(() => { + projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); + }).toThrowError( + expect.objectContaining({ + baseMessage: INVALID_EXPERIMENT_ID, + params: ['invalidExperimentId'], + }) + ); + }); +}); describe('getVariationIdFromExperimentAndVariationKey', () => { let testData: Record; diff --git a/lib/project_config/project_config.tests.js b/lib/project_config/project_config.tests.js index 612e93a12..d69afda46 100644 --- a/lib/project_config/project_config.tests.js +++ b/lib/project_config/project_config.tests.js @@ -402,6 +402,21 @@ describe('lib/core/project_config', function() { ); }); + it('should retrieve traffic allocation given valid experiment key in getTrafficAllocation', function() { + assert.deepEqual( + projectConfig.getTrafficAllocation(configObj, testData.experiments[0].id), + testData.experiments[0].trafficAllocation + ); + }); + + it('should throw error for invalid experient key in getTrafficAllocation', function() { + const ex = assert.throws(function() { + projectConfig.getTrafficAllocation(configObj, 'invalidExperimentId'); + }); + assert.equal(ex.baseMessage, INVALID_EXPERIMENT_ID); + assert.deepEqual(ex.params, ['invalidExperimentId']); + }); + describe('#getVariationIdFromExperimentAndVariationKey', function() { it('should return the variation id for the given experiment key and variation key', function() { assert.strictEqual( diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts index 14b9613fd..e91c4743a 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -559,6 +559,22 @@ export const getExperimentFromKey = function(projectConfig: ProjectConfig, exper throw new OptimizelyError(EXPERIMENT_KEY_NOT_IN_DATAFILE, experimentKey); }; + +/** + * Given an experiment id, returns the traffic allocation within that experiment + * @param {ProjectConfig} projectConfig Object representing project configuration + * @param {string} experimentId Id representing the experiment + * @return {TrafficAllocation[]} Traffic allocation for the experiment + * @throws If experiment key is not in datafile + */ +export const getTrafficAllocation = function(projectConfig: ProjectConfig, experimentId: string): TrafficAllocation[] { + const experiment = projectConfig.experimentIdMap[experimentId]; + if (!experiment) { + throw new OptimizelyError(INVALID_EXPERIMENT_ID, experimentId); + } + return experiment.trafficAllocation; +}; + /** * Get experiment from provided experiment id. Log an error if no experiment * exists in the project config with the given ID. @@ -879,4 +895,5 @@ export default { isFeatureExperiment, toDatafile, tryCreatingProjectConfig, + getTrafficAllocation, };