Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[25.x] AppSource installation changes. #2468

Merged
merged 3 commits into from
Dec 10, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,10 @@ page 2516 "AppSource Product Details"
var
ExtensionManagement: Codeunit "Extension Management";
begin
if (PlansAreVisible) then
if PlansAreVisible then
if not Confirm(PurchaseLicensesElsewhereLbl) then
exit;

ExtensionManagement.InstallMarketplaceExtension(AppID);
end;
}
Expand Down Expand Up @@ -305,7 +306,7 @@ page 2516 "AppSource Product Details"
PlansOverview := '';
end;

CurrentRecordCanBeInstalled := (AppID <> '') and (not CurrentRecordCanBeUninstalled) and AppSourceProductManager.CanInstallProductWithPlans(AllPlans);
CurrentRecordCanBeInstalled := (AppID <> '') and (not CurrentRecordCanBeUninstalled) and AppSourceProductManager.CanInstallProductWithPlans(UniqueProductID);
end;

local procedure BuildPlanPriceText(Availabilities: JsonArray; var MonthlyPriceText: Text; var YearlyPriceText: Text): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ codeunit 2515 "AppSource Product Manager"
CatalogProductsUriLbl: Label 'https://catalogapi.azure.com/products', Locked = true;
CatalogApiVersionQueryParamNameLbl: Label 'api-version', Locked = true;
CatalogApiVersionQueryParamValueLbl: Label '2023-05-01-preview', Locked = true;
CatalogApiVersionOldQueryParamValueLbl: Label '2018-08-01-beta', Locked = true;
CatalogApiOrderByQueryParamNameLbl: Label '$orderby', Locked = true;
CatalogMarketQueryParamNameLbl: Label 'market', Locked = true;
CatalogListMarketNameLbl: label 'all', Locked = true;
CatalogLanguageQueryParamNameLbl: Label 'language', Locked = true;
CatalogApiFilterQueryParamNameLbl: Label '$filter', Locked = true;
CatalogApiSelectQueryParamNameLbl: Label '$select', Locked = true;
Expand Down Expand Up @@ -92,54 +94,46 @@ codeunit 2515 "AppSource Product Manager"
/// <summary>
/// Checks if the product can be installed or your are required to perform operations on AppSource before you can install the product.
/// </summary>
/// <param name="Plans">JSonArray representing the product plans</param>
/// <returns>True if the product can be installed, otherwise false</returns>
internal procedure CanInstallProductWithPlans(Plans: JsonArray): Boolean
internal procedure CanInstallProductWithPlans(UniqieProductIDValue: Text): Boolean
var
LegacyProductObject: JsonObject;
LegacyPlansToken: JsonToken;
LegacyPlans: JsonArray;
PlanToken: JsonToken;
PlanObject: JsonObject;
PricingTypesToken: JsonToken;
PricingTypes: JsonArray;
PricingType: JsonToken;
CallToActionToken: JsonToken;
begin
foreach PlanToken in Plans do begin
PlanObject := PlanToken.AsObject();
Init();

if PlanObject.Get('pricingTypes', PricingTypesToken) then
if (PricingTypesToken.IsArray()) then begin
PricingTypes := PricingTypesToken.AsArray();
if PricingTypes.Count() = 0 then
exit(false); // No price structure, you need to contact the publisher

foreach PricingType in PricingTypes do
case LowerCase(PricingType.AsValue().AsText()) of
'free', // Free
'freetrial', // Free trial
'payg', // Pay as you go
'byol': // Bring your own license
exit(true);
end;
end;
// Query legacy api toget all the plan and test if there is a contact me call to action.
LegacyProductObject := GetProductDetails(UniqieProductIDValue, ConstructProductUri(UniqieProductIDValue, CatalogApiVersionOldQueryParamValueLbl));
LegacyProductObject.Get('plans', LegacyPlansToken);
LegacyPlans := LegacyPlansToken.AsArray();
foreach PlanToken in LegacyPlans do begin
PlanObject := PlanToken.AsObject();
if PlanObject.Get('callToAction', CallToActionToken) then
if LowerCase(CallToActionToken.AsValue().AsText()) = 'contactme' then
exit(false);
end;

exit(false);
exit(true);
end;
#endregion

#region Market and language helper functions
procedure GetCurrentLanguageCultureName(): Text
local procedure GetCurrentLanguageCultureName(): Text
var
Language: Codeunit Language;
begin
exit(Language.GetCultureName(GetCurrentUserLanguageID()));
end;

procedure ResolveMarketAndLanguage(var Market: Code[2]; var LanguageName: Text)
local procedure ResolveLanguageName() LanguageName: Text;
var
Language: Codeunit Language;
LanguageID: Integer;
begin
GetCurrentUserLanguageAndLocaleID(LanguageID, Market);
LanguageID := GetCurrentUserLanguageAndLocaleID();

// Marketplace API only supports two letter languages.
LanguageName := Language.GetTwoLetterISOLanguageName(LanguageID);
Expand All @@ -161,7 +155,7 @@ codeunit 2515 "AppSource Product Manager"
exit(LanguageID);
end;

local procedure GetCurrentUserLanguageAndLocaleID(var LanguageID: Integer; var LocaleID: Code[2])
local procedure GetCurrentUserLanguageAndLocaleID() LanguageID: Integer
var
TempUserSettings: Record "User Settings" temporary;
Language: Codeunit Language;
Expand All @@ -172,9 +166,6 @@ codeunit 2515 "AppSource Product Manager"
LanguageID := Language.GetLanguageIdFromCultureName(AppSourceProductManagerDependencies.GetPreferredLanguage());
if (LanguageID = 0) then
LanguageID := 1033; // Default to EN-US

if (AppSourceProductManagerDependencies.IsSaas()) then
LocaleID := AppSourceProductManagerDependencies.GetCountryLetterCode();
end;

/// <summary>
Expand Down Expand Up @@ -229,14 +220,21 @@ codeunit 2515 "AppSource Product Manager"
/// </summary>
local procedure GetProductDetails(UniqueProductIDValue: Text): JsonObject
var
RestClient: Codeunit "Rest Client";
RequestUri: Text;
begin
Init();
RequestUri := ConstructProductUri(UniqueProductIDValue);
exit(GetProductDetails(UniqueProductIDValue, RequestUri));
end;

local procedure GetProductDetails(UniqueProductIDValue: Text; RequestUri: Text): JsonObject
var
RestClient: Codeunit "Rest Client";
ClientRequestID: Guid;
TelemetryDictionary: Dictionary of [Text, Text];
begin
Init();
ClientRequestID := CreateGuid();
RequestUri := ConstructProductUri(UniqueProductIDValue);

PopulateTelemetryDictionary(ClientRequestID, UniqueProductIDValue, RequestUri, TelemetryDictionary);
Session.LogMessage('AL:AppSource-GetProduct', 'Requesting product details.', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, TelemetryDictionary);
Expand Down Expand Up @@ -297,13 +295,12 @@ codeunit 2515 "AppSource Product Manager"
UriBuilder: Codeunit "Uri Builder";
Uri: Codeunit Uri;
Language: Text;
Market: Code[2];
begin
ResolveMarketAndLanguage(Market, Language);
Language := ResolveLanguageName();

UriBuilder.Init(CatalogProductsUriLbl);
UriBuilder.AddQueryParameter(CatalogApiVersionQueryParamNameLbl, CatalogApiVersionQueryParamValueLbl);
UriBuilder.AddQueryParameter(CatalogMarketQueryParamNameLbl, Market);
UriBuilder.AddQueryParameter(CatalogMarketQueryParamNameLbl, CatalogListMarketNameLbl);
UriBuilder.AddQueryParameter(CatalogLanguageQueryParamNameLbl, Language);

UriBuilder.AddODataQueryParameter(CatalogApiFilterQueryParamNameLbl, 'productType eq ''DynamicsBC''');
Expand All @@ -315,16 +312,23 @@ codeunit 2515 "AppSource Product Manager"
end;

local procedure ConstructProductUri(UniqueIdentifier: Text): Text
begin
exit(ConstructProductUri(UniqueIdentifier, CatalogApiVersionQueryParamValueLbl));
end;

local procedure ConstructProductUri(UniqueIdentifier: Text; ApiVersion: Text): Text
var
UriBuilder: Codeunit "Uri Builder";
Uri: Codeunit Uri;
Language: Text;
Market: Code[2];
Market: Text;
begin
ResolveMarketAndLanguage(Market, Language);
// For market in the product details we use the Entra ID country code.
Market := AppSourceProductManagerDependencies.GetCountryLetterCode();
Language := ResolveLanguageName();
UriBuilder.Init(CatalogProductsUriLbl);
UriBuilder.SetPath('products/' + UniqueIdentifier);
UriBuilder.AddQueryParameter(CatalogApiVersionQueryParamNameLbl, CatalogApiVersionQueryParamValueLbl);
UriBuilder.AddQueryParameter(CatalogApiVersionQueryParamNameLbl, ApiVersion);
UriBuilder.AddQueryParameter(CatalogMarketQueryParamNameLbl, Market);
UriBuilder.AddQueryParameter(CatalogLanguageQueryParamNameLbl, Language);
UriBuilder.GetUri(Uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,6 @@ codeunit 132935 "AppSrc Product Mgr. Test Impl."
exit(AppSourceProductManager.ExtractAppIDFromUniqueProductID(UniqueProductIDValue))
end;

procedure CanInstallProductWithPlans(Plans: JsonArray): Boolean
begin
exit(AppSourceProductManager.CanInstallProductWithPlans(Plans));
end;

#region Record handling functions

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,82 +172,6 @@ codeunit 135074 "AppSource Gallery Test"
AppSrcProductMgrTestImpl.ResetDependencies();
end;

[Test]
// In AppSource this shows up as having the Buy Now button enabled
procedure TestCanInstallProduct_BuyNow()
var
PlansList: JsonArray;
CanInstall: Boolean;
begin
// Given
// Hello world sample: PUBID.microsoftdynsmb%7CAID.helloworld%7CPAPPID.8e315acc-413d-46d5-abb9-c16912d3f3e3
PlansList.ReadFrom('[{"id": "0002","availabilities": [{"id": "DZH318Z0BMGT","actions": ["Browse","Curate","Details","License","Purchase"],"meter": null,"pricingAudience": "DirectCommercial","terms": [{"termDescriptionParameters": null,"termId": "bh3541oe15ry","termUnit": "P1M","prorationPolicy": {"minimumProratedUnits": "P1D"},"termDescription": "1 Month Trial to 1 Year Subscription","price": {"currencyCode": "USD","isPIRequired": true,"listPrice": 0.0,"msrp": 0.0},"renewTermId": "qdp73gtwa5dy","renewTermUnits": "P1Y","isAutorenewable": true},{"termDescriptionParameters": null,"termId": "njspcsugneyy","termUnit": "P1M","prorationPolicy": {"minimumProratedUnits": "P1D"},"termDescription": "1 Month Trial to 1 Month Subscription","price": {"currencyCode": "USD","isPIRequired": true,"listPrice": 0.0,"msrp": 0.0},"renewTermId": "usrac41besqy","renewTermUnits": "P1M","isAutorenewable": true}],"hasFreeTrials": true,"consumptionUnitType": "DAY","displayRank": 0},{"id": "DZH318Z0BMGP","actions": ["Browse","Curate","Details","License","Purchase","Renew"],"meter": null,"pricingAudience": "DirectCommercial","terms": [{"termDescriptionParameters": null,"termId": "qdp73gtwa5dy","termUnit": "P1Y","prorationPolicy": {"minimumProratedUnits": "P1D"},"termDescription": "1 Year Subscription","price": {"currencyCode": "USD","isPIRequired": true,"listPrice": 0.02,"msrp": 0.02},"renewTermId": "qdp73gtwa5dy","renewTermUnits": "P1Y","isAutorenewable": true},{"termDescriptionParameters": null,"termId": "usrac41besqy","termUnit": "P1M","prorationPolicy": {"minimumProratedUnits": "P1D"},"termDescription": "1 Month Subscription","price": {"currencyCode": "USD","isPIRequired": true,"listPrice": 0.01,"msrp": 0.01},"renewTermId": "usrac41besqy","renewTermUnits": "P1M","isAutorenewable": true}],"hasFreeTrials": false,"consumptionUnitType": "DAY","displayRank": 1}],"uiDefinitionUri": "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RW15J0G","isHidden": false,"isStopSell": false,"cspState": "OptOut","minQuantity": 1,"maxQuantity": 10,"isQuantifiable": true,"purchaseDurationDiscounts": [],"planId": "transactableplan1","uniquePlanId": "microsoftdynsmb.helloworldtransactableplan1","displayName": "First 10 users","metadata": {"generation": null,"altStackReference": null},"categoryIds": [],"pricingTypes": ["FreeTrial","Payg"],"description": "Test plan to test first 10 users configurations","skuId": "0002","planType": "DynamicsBC","displayRank": "2147483647","isPrivate": false},{"id": "0003","availabilities": [{"id": "DZH318Z0BMGW","actions": ["Browse","Curate","Details","License","Purchase","Renew"],"meter": null,"pricingAudience": "DirectCommercial","terms": [{"termDescriptionParameters": null,"termId": "qdp73gtwa5dy","termUnit": "P1Y","prorationPolicy": {"minimumProratedUnits": "P1D"},"termDescription": "1 Year Subscription","price": {"currencyCode": "USD","isPIRequired": true,"listPrice": 0.03,"msrp": 0.03},"renewTermId": "qdp73gtwa5dy","renewTermUnits": "P1Y","isAutorenewable": true},{"termDescriptionParameters": null,"termId": "usrac41besqy","termUnit": "P1M","prorationPolicy": {"minimumProratedUnits": "P1D"},"termDescription": "1 Month Subscription","price": {"currencyCode": "USD","isPIRequired": true,"listPrice": 0.02,"msrp": 0.02},"renewTermId": "usrac41besqy","renewTermUnits": "P1M","isAutorenewable": true}],"hasFreeTrials": false,"consumptionUnitType": "DAY","displayRank": 0}],"uiDefinitionUri": "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RW15J0H","isHidden": false,"isStopSell": false,"cspState": "OptOut","minQuantity": 10,"maxQuantity": 100,"isQuantifiable": true,"purchaseDurationDiscounts": [],"planId": "transactableplan2","uniquePlanId": "microsoftdynsmb.helloworldtransactableplan2","displayName": "Ten to Hundred plan","metadata": {"generation": null,"altStackReference": null},"categoryIds": [],"pricingTypes": ["Payg"],"description": "Test 10 - 100 User plan","skuId": "0003","planType": "DynamicsBC","displayRank": "2147483647","isPrivate": false}]');

// When
CanInstall := AppSrcProductMgrTestImpl.CanInstallProductWithPlans(PlansList);

// Then
LibraryAssert.IsTrue(CanInstall, 'The product should be installable.');
end;

[Test]
// In AppSource this shows up as having the Get It Now button enabled
procedure TestCanInstallProduct_GetItNow()
var
PlansList: JsonArray;
CanInstall: Boolean;
begin
// Given
// Hello world too sample: PUBID.microsoftdynsmb%7CAID.helloworldtoo%7CPAPPID.37447a59-b131-4e9c-83a3-a7856bfc30ff
PlansList.ReadFrom('[{"id": "0001","availabilities": [{"id": "DZH318Z0BMTW","actions": ["Browse","Curate","Details"],"meter": null,"pricingAudience": "DirectCommercial","terms": null,"hasFreeTrials": false,"displayRank": 0}],"uiDefinitionUri": "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE5c9A4","isHidden": false,"isStopSell": false,"isQuantifiable": false,"purchaseDurationDiscounts": [],"planId": "69fe2a5e-cb01-43ea-9af3-2417d0c843f2","uniquePlanId": "microsoftdynsmb.helloworldtoo69fe2a5e-cb01-43ea-9af3-2417d0c843f2","displayName": "HelloWorldToo","metadata": {"generation": null,"altStackReference": null},"categoryIds": [],"pricingTypes": [],"description": "<div>desc</div>","skuId": "0001","planType": "DynamicsBC","isPrivate": false}]');

// When
CanInstall := AppSrcProductMgrTestImpl.CanInstallProductWithPlans(PlansList);

// Then
if (CanInstall) then
LibraryAssert.Fail('Test now produces expected outcome and should be update.');
end;


[Test]
// In AppSource this shows up as having the Free Trial button enabled
procedure TestCanInstallProduct_FreeTrial()
var
PlansList: JsonArray;
CanInstall: Boolean;
begin
// Given
// Manufacturing Central by Intech: PUBID.intechsystems%7CAID.manufacturing_central%7CPAPPID.cea8e27e-050e-4880-840c-954ceb2e3f13
PlansList.ReadFrom('[{"id": "0001","availabilities": [{"id": "DZH318Z0BMV3","actions": ["Browse","Curate","Details"],"meter": null,"pricingAudience": "DirectCommercial","terms": null,"hasFreeTrials": false,"displayRank": 0}],"uiDefinitionUri": "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RW1fI7S","isHidden": false,"isStopSell": false,"isQuantifiable": false,"purchaseDurationDiscounts": [],"planId": "77447b7c-9737-4132-8191-b9ce21d35a0e","uniquePlanId": "intechsystems.manufacturing_central77447b7c-9737-4132-8191-b9ce21d35a0e","displayName": "Manufacturing Central","metadata": {"generation": null,"altStackReference": null},"categoryIds": [],"pricingTypes": [],"description": "<p>Hey there</p>","skuId": "0001","planType": "DynamicsBC","isPrivate": false}]');

// When
CanInstall := AppSrcProductMgrTestImpl.CanInstallProductWithPlans(PlansList);

// Then
if (CanInstall) then
LibraryAssert.Fail('Test now produces expected outcome and should be update.');
end;

[Test]

// In AppSource this shows up as having the Contact Me button enabled
procedure TestCanInstallProduct_ContactMe()
var
PlansList: JsonArray;
CanInstall: Boolean;
begin
// Given
// Salesforce Integration by Celigo Inc: PUBID.celigoinc-causa1621285384596%7CAID.salesforce-integration-celigo%7CPAPPID.0595b87b-7670-4bd2-91e2-bd98a7fe2f5a
PlansList.ReadFrom('[{"id": "0001","availabilities": [{"id": "DZH318Z0BMV7","actions": ["Browse","Curate","Details"],"meter": null,"pricingAudience": "DirectCommercial","terms": null,"hasFreeTrials": false,"displayRank": 0}],"uiDefinitionUri": "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE5fzzR","isHidden": false,"isStopSell": false,"isQuantifiable": false,"purchaseDurationDiscounts": [],"planId": "4b4a5c29-40ad-4169-b23c-1ea9a51fd176","uniquePlanId": "celigoinc-causa1621285384596.salesforce-integration-celigo4b4a5c29-40ad-4169-b23c-1ea9a51fd176","displayName": "Salesforce Integration for Dynamics 365 Business Central","metadata": {"generation": null,"altStackReference": null},"categoryIds": [],"pricingTypes": [],"description": "<div>Celigo</div>","skuId": "0001","planType": "DynamicsBC","isPrivate": false}]');

// When
CanInstall := AppSrcProductMgrTestImpl.CanInstallProductWithPlans(PlansList);

// Then
LibraryAssert.IsFalse(CanInstall, 'The product NOT should be installable.');
end;

[HyperlinkHandler]
procedure HyperlinkHandler(Message: Text[1024])
begin
Expand Down
Loading