Skip to content

Commit

Permalink
Backport
Browse files Browse the repository at this point in the history
  • Loading branch information
SBalslev committed Nov 11, 2024
1 parent cdac9fb commit 8d16e28
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 117 deletions.
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 @@ -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

0 comments on commit 8d16e28

Please sign in to comment.