Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions src/support/slack/actions/entitlement-modal-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ export async function updateMessageToProcessing(client, channelId, messageTs, ba
}

/**
* Creates entitlements for selected products in parallel
* Creates entitlements for selected products in parallel.
* If the org already has an entitlement for a product, preserves the existing tier
* and only creates the site enrollment if missing.
* @param {object} lambdaContext - Lambda context
* @param {object} site - Site object
* @param {string[]} selectedProducts - Array of product codes
Expand All @@ -177,6 +179,36 @@ export async function updateMessageToProcessing(client, channelId, messageTs, ba
export async function createEntitlementsForProducts(lambdaContext, site, selectedProducts) {
const entitlementPromises = selectedProducts.map(async (product) => {
const tierClient = await TierClient.createForSite(lambdaContext, site, product);
const existing = await tierClient.checkValidEntitlement();

if (existing.entitlement) {
const currentTier = existing.entitlement.getTier();

if (existing.siteEnrollment) {
return {
product,
entitlementId: existing.entitlement.getId(),
enrollmentId: existing.siteEnrollment.getId(),
existingTier: currentTier,
alreadyExisted: true,
};
}

const { SiteEnrollment } = lambdaContext.dataAccess;
const siteEnrollment = await SiteEnrollment.create({
siteId: site.getId(),
entitlementId: existing.entitlement.getId(),
});

return {
product,
entitlementId: existing.entitlement.getId(),
enrollmentId: siteEnrollment.getId(),
existingTier: currentTier,
enrollmentCreated: true,
};
}

const { entitlement, siteEnrollment } = await tierClient.createEntitlement(
EntitlementModel.TIERS.FREE_TRIAL,
);
Expand All @@ -200,8 +232,17 @@ export async function createEntitlementsForProducts(lambdaContext, site, selecte
export async function postEntitlementMessages(say, entitlementResults, siteId) {
/* eslint-disable no-await-in-loop */
for (const result of entitlementResults) {
const message = `:white_check_mark: Ensured ${result.product} entitlement ${result.entitlementId} `
+ `(${EntitlementModel.TIERS.FREE_TRIAL}) and enrollment ${result.enrollmentId} for site ${siteId}`;
let message;
if (result.alreadyExisted) {
message = `:information_source: ${result.product} entitlement ${result.entitlementId} `
+ `(${result.existingTier}) and enrollment ${result.enrollmentId} already exist for site ${siteId}`;
} else if (result.enrollmentCreated) {
message = `:white_check_mark: ${result.product} entitlement ${result.entitlementId} `
+ `(${result.existingTier}) already existed — created enrollment ${result.enrollmentId} for site ${siteId}`;
} else {
message = `:white_check_mark: Created ${result.product} entitlement ${result.entitlementId} `
+ `(${EntitlementModel.TIERS.FREE_TRIAL}) and enrollment ${result.enrollmentId} for site ${siteId}`;
}
await say(message);
}
/* eslint-enable no-await-in-loop */
Expand Down
37 changes: 25 additions & 12 deletions src/support/slack/actions/entitlement-modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,18 +294,31 @@ export function ensureEntitlementImsOrgModal(lambdaContext) {
for (const product of selectedProducts) {
try {
const tierClient = TierClient.createForOrg(lambdaContext, organization, product);
const { entitlement } = await tierClient.createEntitlement(
EntitlementModel.TIERS.FREE_TRIAL,
);
entitlementResults.push({
product,
entitlementId: entitlement.getId(),
});

await say(
`:white_check_mark: Ensured ${product} entitlement ${entitlement.getId()} `
+ `(${EntitlementModel.TIERS.FREE_TRIAL}) for organization ${organizationId}`,
);
const existing = await tierClient.checkValidEntitlement();

if (existing.entitlement) {
const currentTier = existing.entitlement.getTier();
entitlementResults.push({
product,
entitlementId: existing.entitlement.getId(),
});
await say(
`:information_source: ${product} entitlement ${existing.entitlement.getId()} `
+ `(${currentTier}) already exists for organization ${organizationId}`,
);
} else {
const { entitlement } = await tierClient.createEntitlement(
EntitlementModel.TIERS.FREE_TRIAL,
);
entitlementResults.push({
product,
entitlementId: entitlement.getId(),
});
await say(
`:white_check_mark: Created ${product} entitlement ${entitlement.getId()} `
+ `(${EntitlementModel.TIERS.FREE_TRIAL}) for organization ${organizationId}`,
);
}
} catch (error) {
log.error(`Error creating ${product} entitlement for org ${organizationId}:`, error);
await say(`:x: Failed to ensure ${product} entitlement: ${error.message}`);
Expand Down
104 changes: 84 additions & 20 deletions test/support/slack/actions/entitlement-modal-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,9 @@ describe('Modal Utils', () => {
});

describe('createEntitlementsForProducts', () => {
it('creates entitlements for multiple products in parallel', async () => {
it('creates new entitlements when none exist', async () => {
const mockSite = { getId: () => 'site123' };
const mockLambdaContext = { env: {}, log: {} };
const mockLambdaContext = { env: {}, log: {}, dataAccess: {} };
const selectedProducts = ['ASO', 'LLMO'];

const mockEntitlement1 = { getId: () => 'entitlement-aso' };
Expand All @@ -279,12 +279,14 @@ describe('Modal Utils', () => {

mockTierClient.createForSite
.onFirstCall().returns({
checkValidEntitlement: sinon.stub().resolves({}),
createEntitlement: sinon.stub().resolves({
entitlement: mockEntitlement1,
siteEnrollment: mockEnrollment1,
}),
})
.onSecondCall().returns({
checkValidEntitlement: sinon.stub().resolves({}),
createEntitlement: sinon.stub().resolves({
entitlement: mockEntitlement2,
siteEnrollment: mockEnrollment2,
Expand All @@ -311,16 +313,16 @@ describe('Modal Utils', () => {
});
});

it('creates entitlement for single product', async () => {
it('preserves existing entitlement and returns existing enrollment', async () => {
const mockSite = { getId: () => 'site456' };
const mockLambdaContext = { env: {}, log: {} };
const mockLambdaContext = { env: {}, log: {}, dataAccess: {} };
const selectedProducts = ['ASO'];

const mockEntitlement = { getId: () => 'entitlement-123' };
const mockEnrollment = { getId: () => 'enrollment-123' };
const mockEntitlement = { getId: () => 'entitlement-existing', getTier: () => 'PAID' };
const mockEnrollment = { getId: () => 'enrollment-existing' };

mockTierClient.createForSite.returns({
createEntitlement: sinon.stub().resolves({
checkValidEntitlement: sinon.stub().resolves({
entitlement: mockEntitlement,
siteEnrollment: mockEnrollment,
}),
Expand All @@ -335,17 +337,60 @@ describe('Modal Utils', () => {
expect(results).to.have.lengthOf(1);
expect(results[0]).to.deep.equal({
product: 'ASO',
entitlementId: 'entitlement-123',
enrollmentId: 'enrollment-123',
entitlementId: 'entitlement-existing',
enrollmentId: 'enrollment-existing',
existingTier: 'PAID',
alreadyExisted: true,
});
});

it('returns empty array for empty product list', async () => {
it('creates enrollment when entitlement exists but enrollment does not', async () => {
const mockSite = { getId: () => 'site789' };
const mockLambdaContext = { env: {}, log: {} };
const mockEnrollment = { getId: () => 'enrollment-new' };
const mockLambdaContext = {
env: {},
log: {},
dataAccess: {
SiteEnrollment: {
create: sinon.stub().resolves(mockEnrollment),
},
},
};
const selectedProducts = ['ASO'];

const mockEntitlement = { getId: () => 'entitlement-existing', getTier: () => 'PLG' };

mockTierClient.createForSite.returns({
checkValidEntitlement: sinon.stub().resolves({
entitlement: mockEntitlement,
}),
});

const results = await modalUtils.createEntitlementsForProducts(
mockLambdaContext,
mockSite,
selectedProducts,
);

expect(results).to.have.lengthOf(1);
expect(results[0]).to.deep.equal({
product: 'ASO',
entitlementId: 'entitlement-existing',
enrollmentId: 'enrollment-new',
existingTier: 'PLG',
enrollmentCreated: true,
});
expect(mockLambdaContext.dataAccess.SiteEnrollment.create).to.have.been.calledWith({
siteId: 'site789',
entitlementId: 'entitlement-existing',
});
});

it('returns empty array for empty product list', async () => {
const mockSite = { getId: () => 'site999' };
const mockLambdaContext = { env: {}, log: {}, dataAccess: {} };
const selectedProducts = [];

// Reset the stub before this test
mockTierClient.createForSite.resetHistory();

const results = await modalUtils.createEntitlementsForProducts(
Expand All @@ -360,7 +405,7 @@ describe('Modal Utils', () => {
});

describe('postEntitlementMessages', () => {
it('posts messages for multiple entitlement results', async () => {
it('posts created message for newly created entitlements', async () => {
const say = sinon.stub().resolves();
const entitlementResults = [
{ product: 'ASO', entitlementId: 'ent-1', enrollmentId: 'enr-1' },
Expand All @@ -371,32 +416,51 @@ describe('Modal Utils', () => {
await modalUtils.postEntitlementMessages(say, entitlementResults, siteId);

expect(say.calledTwice).to.be.true;
expect(say.getCall(0).args[0]).to.include('Created');
expect(say.getCall(0).args[0]).to.include('ASO');
expect(say.getCall(0).args[0]).to.include('ent-1');
expect(say.getCall(0).args[0]).to.include('enr-1');
expect(say.getCall(0).args[0]).to.include('site123');
expect(say.getCall(0).args[0]).to.include(EntitlementModel.TIERS.FREE_TRIAL);

expect(say.getCall(1).args[0]).to.include('Created');
expect(say.getCall(1).args[0]).to.include('LLMO');
expect(say.getCall(1).args[0]).to.include('ent-2');
expect(say.getCall(1).args[0]).to.include('enr-2');
expect(say.getCall(1).args[0]).to.include('site123');
});

it('posts message for single entitlement result', async () => {
it('posts already-existed message when entitlement and enrollment existed', async () => {
const say = sinon.stub().resolves();
const entitlementResults = [
{ product: 'ASO', entitlementId: 'ent-123', enrollmentId: 'enr-456' },
{
product: 'ASO', entitlementId: 'ent-123', enrollmentId: 'enr-456', existingTier: 'PAID', alreadyExisted: true,
},
];
const siteId = 'site789';

await modalUtils.postEntitlementMessages(say, entitlementResults, siteId);

expect(say.calledOnce).to.be.true;
expect(say.getCall(0).args[0]).to.include('ASO');
expect(say.getCall(0).args[0]).to.include('already exist');
expect(say.getCall(0).args[0]).to.include('PAID');
expect(say.getCall(0).args[0]).to.include('ent-123');
expect(say.getCall(0).args[0]).to.include('enr-456');
expect(say.getCall(0).args[0]).to.include('site789');
});

it('posts enrollment-created message when entitlement existed but enrollment was new', async () => {
const say = sinon.stub().resolves();
const entitlementResults = [
{
product: 'ASO', entitlementId: 'ent-123', enrollmentId: 'enr-new', existingTier: 'PLG', enrollmentCreated: true,
},
];
const siteId = 'site789';

await modalUtils.postEntitlementMessages(say, entitlementResults, siteId);

expect(say.calledOnce).to.be.true;
expect(say.getCall(0).args[0]).to.include('already existed');
expect(say.getCall(0).args[0]).to.include('created enrollment');
expect(say.getCall(0).args[0]).to.include('PLG');
expect(say.getCall(0).args[0]).to.include('enr-new');
});

it('does not post messages for empty results', async () => {
Expand Down
47 changes: 44 additions & 3 deletions test/support/slack/actions/entitlement-modals.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ describe('EntitlementModals', () => {
revokeSiteEnrollment: sinon.stub().resolves(),
}),
createForOrg: sinon.stub().returns({
checkValidEntitlement: sinon.stub().resolves({}),
createEntitlement: sinon.stub().resolves({
entitlement: { getId: () => TEST_IDS.ent },
}),
Expand Down Expand Up @@ -397,15 +398,55 @@ describe('EntitlementModals', () => {
);
});

it('preserves existing entitlement and skips creation', async () => {
const existingEntitlement = {
getId: () => 'ent-existing',
getTier: () => 'PLG',
};
const mockTierClientInstance = {
checkValidEntitlement: sinon.stub().resolves({ entitlement: existingEntitlement }),
};

const module = await esmock('../../../../src/support/slack/actions/entitlement-modals.js', {
'@adobe/spacecat-shared-tier-client': {
default: {
createForOrg: sinon.stub().returns(mockTierClientInstance),
},
},
'../../../../src/support/slack/actions/entitlement-modal-utils.js': {
extractSelectedProducts: await import('../../../../src/support/slack/actions/entitlement-modal-utils.js')
.then((m) => m.extractSelectedProducts),
createSayFunction: await import('../../../../src/support/slack/actions/entitlement-modal-utils.js')
.then((m) => m.createSayFunction),
updateMessageToProcessing: mockUpdateMessageToProcessing,
},
});

await testModalSubmission(
module.ensureEntitlementImsOrgModal,
createOrgMetadata(),
createProductState(),
(ack, client) => {
expect(ack).to.have.been.calledOnce;
expect(client.chat.postMessage).to.have.been.calledWith(sinon.match({
text: sinon.match('already exists'),
}));
expect(client.chat.postMessage).to.have.been.calledWith(sinon.match({
text: sinon.match('PLG'),
}));
},
);
});

it('handles errors during individual product entitlement', async () => {
const mockTierClient = {
createEntitlement: sinon.stub().rejects(new Error('Tier error')),
const mockTierClientInstance = {
checkValidEntitlement: sinon.stub().rejects(new Error('Tier error')),
};

const module = await esmock('../../../../src/support/slack/actions/entitlement-modals.js', {
'@adobe/spacecat-shared-tier-client': {
default: {
createForOrg: sinon.stub().returns(mockTierClient),
createForOrg: sinon.stub().returns(mockTierClientInstance),
},
},
'../../../../src/support/slack/actions/entitlement-modal-utils.js': {
Expand Down
Loading