From 51fef02cb679a37d93c55868641a49b5fad6842c Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:12:19 +0200 Subject: [PATCH 01/10] Bitly integration (in progress) --- .../Yoma.Core.Api/appsettings.json | 6 +- .../Core/Extensions/FlurlExtensions.cs | 6 +- .../Services/OrganizationBackgroundService.cs | 6 +- .../Entity/Services/UserBackgroundService.cs | 2 +- .../Lookups/Services/SkillService.cs | 2 +- .../Services/OpportunityBackgroundService.cs | 6 +- .../Services/RewardBackgroundService.cs | 4 +- .../ShortLinkProvider}/Enumerations.cs | 10 +- .../Interfaces/IShortLinkProviderClient.cs | 4 +- .../Models/ShortLinkRequest.cs | 13 ++ .../Models/ShortLinkResponse.cs | 9 + .../Client/BitlyClient.cs | 170 +++++++++++++++--- .../Client/BitlyClientFactory.cs | 8 +- ...inksRequest.cs => BitLinkRequestCreate.cs} | 11 +- .../Models/BitLinkRequestPAtchCustomBack.cs | 13 ++ ...BitLinksResponse.cs => BitLinkResponse.cs} | 8 +- .../Models/BitLinkResponseCustomBack.cs | 13 ++ .../Models/BitlyOptions.cs | 8 +- 18 files changed, 239 insertions(+), 60 deletions(-) rename src/api/src/{infrastructure/Yoam.Core.Infrastructure.Bitly => domain/Yoma.Core.Domain/ShortLinkProvider}/Enumerations.cs (50%) create mode 100644 src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs create mode 100644 src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkResponse.cs rename src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/{BitLinksRequest.cs => BitLinkRequestCreate.cs} (63%) create mode 100644 src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkRequestPAtchCustomBack.cs rename src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/{BitLinksResponse.cs => BitLinkResponse.cs} (80%) create mode 100644 src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkResponseCustomBack.cs diff --git a/src/api/src/application/Yoma.Core.Api/appsettings.json b/src/api/src/application/Yoma.Core.Api/appsettings.json index ea6331531..71f381ccd 100644 --- a/src/api/src/application/Yoma.Core.Api/appsettings.json +++ b/src/api/src/application/Yoma.Core.Api/appsettings.json @@ -234,10 +234,10 @@ }, "Bitly": { + "BaseUrl": "https://api-ssl.bitly.com", "GroupId": "{groupid}", "ApiKey": "{apikey}", - "ShortLinkType": "CustomDomainAndBackHalf", - "DomainCustom": "go.yoma.world", - "Tags": [ "Yoma", "Opportunity", "Shortlink" ] + "GenericDomain": "bit.ly", + "CustomDomain": "go.yoma.world" } } diff --git a/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs b/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs index 8599db011..535c53e71 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs @@ -40,7 +40,7 @@ public static IFlurlRequest WithAuthHeaders(this Url url, Dictionary /// /// - public static async Task EnsureSuccessStatusCodeAsync(this Task response) + public static async Task EnsureSuccessStatusCodeAsync(this Task response, List? AdditionalSuccessStatusCodes = null) { IFlurlResponse resp; @@ -66,7 +66,9 @@ public static async Task EnsureSuccessStatusCodeAsync(this Task< var statusCode = (HttpStatusCode)resp.StatusCode; var message = await resp.ResponseMessage.Content.ReadAsStringAsync(); - if (statusCode == HttpStatusCode.OK) return resp; + var successStatusCodes = AdditionalSuccessStatusCodes ?? []; + if(!successStatusCodes.Contains(HttpStatusCode.OK)) successStatusCodes.Add(HttpStatusCode.OK); + if (successStatusCodes.Contains(statusCode)) return resp; throw new HttpClientException(statusCode, message); } diff --git a/src/api/src/domain/Yoma.Core.Domain/Entity/Services/OrganizationBackgroundService.cs b/src/api/src/domain/Yoma.Core.Domain/Entity/Services/OrganizationBackgroundService.cs index 09431e642..35565429c 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Entity/Services/OrganizationBackgroundService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Entity/Services/OrganizationBackgroundService.cs @@ -81,7 +81,7 @@ public async Task ProcessDeclination() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); _logger.LogInformation("Processing organization declination"); @@ -179,7 +179,7 @@ public async Task ProcessDeletion() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); _logger.LogInformation("Processing organization deletion"); @@ -240,7 +240,7 @@ public async Task SeedLogoAndDocuments() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); if (!_appSettings.TestDataSeedingEnvironmentsAsEnum.HasFlag(_environmentProvider.Environment)) { diff --git a/src/api/src/domain/Yoma.Core.Domain/Entity/Services/UserBackgroundService.cs b/src/api/src/domain/Yoma.Core.Domain/Entity/Services/UserBackgroundService.cs index e27991499..711642375 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Entity/Services/UserBackgroundService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Entity/Services/UserBackgroundService.cs @@ -59,7 +59,7 @@ public async Task SeedPhotos() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); if (!_appSettings.TestDataSeedingEnvironmentsAsEnum.HasFlag(_environmentProvider.Environment)) { diff --git a/src/api/src/domain/Yoma.Core.Domain/Lookups/Services/SkillService.cs b/src/api/src/domain/Yoma.Core.Domain/Lookups/Services/SkillService.cs index abcb43ec2..a29f26d1a 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Lookups/Services/SkillService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Lookups/Services/SkillService.cs @@ -122,7 +122,7 @@ public async Task SeedSkills(bool onStartupInitialSeeding) using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); try { diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityBackgroundService.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityBackgroundService.cs index b7194e75f..9a150f8f5 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityBackgroundService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityBackgroundService.cs @@ -72,7 +72,7 @@ public async Task ProcessExpiration() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); _logger.LogInformation("Processing opportunity expiration"); @@ -134,7 +134,7 @@ public async Task ProcessExpirationNotifications() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); _logger.LogInformation("Processing opportunity expiration notifications"); @@ -184,7 +184,7 @@ public async Task ProcessDeletion() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); _logger.LogInformation("Processing opportunity deletion"); diff --git a/src/api/src/domain/Yoma.Core.Domain/Reward/Services/RewardBackgroundService.cs b/src/api/src/domain/Yoma.Core.Domain/Reward/Services/RewardBackgroundService.cs index 4c20cd7e4..16f02d97d 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Reward/Services/RewardBackgroundService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Reward/Services/RewardBackgroundService.cs @@ -72,7 +72,7 @@ public async Task ProcessWalletCreation() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); _logger.LogInformation("Processing Reward wallet creation"); @@ -154,7 +154,7 @@ public async Task ProcessRewardTransactions() using (JobStorage.Current.GetConnection().AcquireDistributedLock(lockIdentifier, lockDuration)) { _logger.LogInformation("Lock '{lockIdentifier}' acquired by {hostName} at {dateStamp}. Lock duration set to {lockDurationInMinutes} minutes", - lockIdentifier, System.Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); + lockIdentifier, Environment.MachineName, DateTimeOffset.UtcNow, lockDuration.TotalMinutes); _logger.LogInformation("Processing reward transactions"); diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Enumerations.cs b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Enumerations.cs similarity index 50% rename from src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Enumerations.cs rename to src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Enumerations.cs index 76d826d1c..08a870615 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Enumerations.cs +++ b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Enumerations.cs @@ -1,24 +1,24 @@ -namespace Yoma.Core.Infrastructure.Bitly +namespace Yoma.Core.Domain.ShortLinkProvider { public enum ShortLinkType { /// - /// Bitly's generic domain, no customized back-half + /// Generic domain and no customized back-half /// Generic, /// - /// A custom domain, but no customized back-half + /// Custom domain and no customized back-half /// CustomDomain, /// - /// Bitly's generic domain, with a customized back-half + /// Generic domain and a customized back-half /// CustomBackHalf, /// - /// A custom domain and a customized back-half + /// Custom domain and a customized back-half /// CustomDomainAndBackHalf, } diff --git a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Interfaces/IShortLinkProviderClient.cs b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Interfaces/IShortLinkProviderClient.cs index 773d8f04d..99d1322cc 100644 --- a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Interfaces/IShortLinkProviderClient.cs +++ b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Interfaces/IShortLinkProviderClient.cs @@ -1,8 +1,10 @@ +using Yoma.Core.Domain.ShortLinkProvider.Models; + namespace Yoma.Core.Domain.ShortLinkProvider.Interfaces { public interface IShortLinkProviderClient { - + Task CreateShortLink(ShortLinkRequest request); } } diff --git a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs new file mode 100644 index 000000000..ab7f61711 --- /dev/null +++ b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs @@ -0,0 +1,13 @@ +namespace Yoma.Core.Domain.ShortLinkProvider.Models +{ + public class ShortLinkRequest + { + public ShortLinkType Type { get; set; } + + public string Title { get; set; } + + public string URL { get; set; } + + public List? Tags { get; set; } + } +} diff --git a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkResponse.cs b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkResponse.cs new file mode 100644 index 000000000..7d30cda3f --- /dev/null +++ b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkResponse.cs @@ -0,0 +1,9 @@ +namespace Yoma.Core.Domain.ShortLinkProvider.Models +{ + public class ShortLinkResponse + { + public string Id { get; set; } + + public string Link { get; set; } + } +} diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index ece55ac5d..55f67e10c 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -1,6 +1,12 @@ -using Microsoft.Extensions.Logging; +using Flurl; +using Flurl.Http; +using System.Net; +using Yoma.Core.Domain.Core.Exceptions; +using Yoma.Core.Domain.Core.Extensions; using Yoma.Core.Domain.Core.Interfaces; +using Yoma.Core.Domain.ShortLinkProvider; using Yoma.Core.Domain.ShortLinkProvider.Interfaces; +using Yoma.Core.Domain.ShortLinkProvider.Models; using Yoma.Core.Infrastructure.Bitly.Models; namespace Yoma.Core.Infrastructure.Bitly.Client @@ -8,56 +14,170 @@ namespace Yoma.Core.Infrastructure.Bitly.Client public class BitlyClient : IShortLinkProviderClient { #region Class Variables - private readonly ILogger _logger; private readonly IEnvironmentProvider _environmentProvider; private readonly BitlyOptions _options; + + private const string Header_Authorization = "Authorization"; + private const string Header_Authorization_Value_Prefix = "Bearer"; + + private readonly static string[] Common_Words = [ + "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", + "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can", "cant", "cannot", + "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", + "for", "from", "further", "had", "hadnt", "has", "hasnt", "have", "havent", "having", "he", "hed", "hell", "hes", + "her", "here", "heres", "hers", "herself", "him", "himself", "his", "how", "hows", "i", "id", "ill", "im", "ive", + "if", "in", "into", "is", "isnt", "it", "its", "its", "itself", "lets", "me", "more", "most", "mustnt", "my", + "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", + "ourselves", "out", "over", "own", "same", "shant", "she", "shed", "shell", "shes", "should", "shouldnt", "so", + "some", "such", "than", "that", "thats", "the", "their", "theirs", "them", "themselves", "then", "there", + "theres", "these", "they", "theyd", "theyll", "theyre", "theyve", "this", "those", "through", "to", "too", + "under", "until", "up", "very", "was", "wasnt", "we", "wed", "well", "were", "weve", "werent", "what", "whats", + "when", "whens", "where", "wheres", "which", "while", "who", "whos", "whom", "why", "whys", "will", "with", "wont", + "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves"]; #endregion #region Constructor - public BitlyClient(ILogger logger, - IEnvironmentProvider environmentProvider, + public BitlyClient(IEnvironmentProvider environmentProvider, BitlyOptions options) { - _logger = logger; _environmentProvider = environmentProvider; _options = options; } #endregion #region Public Members - public string CreateShortLink(string title, string url, string? titleUrl) + public async Task CreateShortLink(ShortLinkRequest request) { - ArgumentException.ThrowIfNullOrEmpty(title, nameof(title)); + ArgumentNullException.ThrowIfNull(request, nameof(request)); + + if (string.IsNullOrWhiteSpace(request.Title)) + throw new ArgumentNullException(nameof(request), "Title is required"); + request.Title = request.Title.Trim().RemoveSpecialCharacters(); + + if (string.IsNullOrWhiteSpace(request.URL)) + throw new ArgumentNullException(nameof(request), "URL is required"); + request.URL = request.URL.Trim(); - if (!Uri.IsWellFormedUriString(url, UriKind.Absolute)) - throw new ArgumentException("Invalid URL", nameof(url)); + if (!Uri.IsWellFormedUriString(request.URL, UriKind.Absolute)) + throw new ArgumentException("Invalid URL", nameof(request)); - var request = new BitLinksRequest + request.Tags = (request.Tags ?? []).Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()) + .Append(_environmentProvider.Environment.ToString()) + .Distinct(StringComparer.InvariantCultureIgnoreCase) + .ToList(); + + var environmentPrefix = _environmentProvider.Environment != Domain.Core.Environment.Production ? $"{_environmentProvider.Environment}: " : string.Empty; + var requestCreate = new BitLinkRequestCreate { - LongURL = url, + LongURL = request.URL, GroupId = _options.GroupId, - Title = title, - Tags = _options.Tags + Title = request.Title, + Tags = request.Tags, + Domain = request.Type switch + { + ShortLinkType.Generic or ShortLinkType.CustomBackHalf => _options.GenericDomain, + ShortLinkType.CustomDomain or ShortLinkType.CustomDomainAndBackHalf => _options.CustomDomain, + _ => throw new ArgumentOutOfRangeException(nameof(request), request.Type, "Invalid type"), + } }; - switch (_options.ShortLinkType) - { - case ShortLinkType.Generic: - break; + var response = await _options.BaseUrl + .AppendPathSegment($"v4/bitlinks") + .WithAuthHeader(GetAuthHeader()) + .PostJsonAsync(requestCreate) + .EnsureSuccessStatusCodeAsync([HttpStatusCode.Created]) + .ReceiveJson(); - case ShortLinkType.CustomDomain: - request.Domain = _options.DomainCustom; - break; + return await UpdateCustomBackHalve(response, request.Type, request.Title); + } + #endregion - case ShortLinkType.CustomBackHalf: - break; + #region Private Members + private KeyValuePair GetAuthHeader() + { + return new KeyValuePair(Header_Authorization, $"{Header_Authorization_Value_Prefix} {_options.ApiKey}"); + } + + private async Task UpdateCustomBackHalve(BitLinkResponse bitLink, ShortLinkType type, string title) + { + var result = new ShortLinkResponse + { + Id = bitLink.Id, + Link = bitLink.Link + }; + switch (type) + { + case ShortLinkType.CustomBackHalf: case ShortLinkType.CustomDomainAndBackHalf: - request.Domain = _options.DomainCustom; - break; + var requestPatch = new BitLinkRequestPAtchCustomBack + { + Id = bitLink.Id, + CustomLink = await GenerateNewUriRelativeWithCustomBackHalf(result.Link, title) + + }; + + var response = await _options.BaseUrl + .AppendPathSegment($"v4/custom_bitlinks") + .WithAuthHeader(GetAuthHeader()) + .PostJsonAsync(requestPatch) + .EnsureSuccessStatusCodeAsync() + .ReceiveJson(); + + result.Id = response.BitLink.Id; + result.Link = response.CustomBitLink; + return result; + + default: + return result; } + } - return string.Empty; + private async Task GenerateNewUriRelativeWithCustomBackHalf(string url, string title) + { + var titleWords = title.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); + var filteredWords = titleWords.Except(Common_Words).Distinct().Select(word => word.TitleCase()).ToList(); + var titleSquashed = filteredWords.Aggregate(string.Empty, (acc, word) => (acc.Length + word.Length <= 50) ? acc + word : acc); + + var newUriRelative = ReplaceLastSegmentAndRemoveScheme(url, titleSquashed); + var result = newUriRelative; + + var suffix = 1; + var maxAttempts = 10; + do + { + try + { + var resp = await _options.BaseUrl + .AppendPathSegment($"v4/bitlinks/{result}") + .WithAuthHeader(GetAuthHeader()) + .GetAsync() + .EnsureSuccessStatusCodeAsync(); + } + catch (HttpClientException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) break; + } + + if (suffix >= maxAttempts) + throw new InvalidOperationException("Maximum attempts reached, unique URL cannot be generated"); + + result = $"{newUriRelative}_{suffix}"; + suffix++; + + } while (true); + + return result; + } + + public static string ReplaceLastSegmentAndRemoveScheme(string url, string newLastSegment) + { + var uri = new Uri(url, UriKind.RelativeOrAbsolute); + var segments = uri.AbsolutePath.Trim('/').Split('/'); + segments[^1] = newLastSegment; + var newPath = string.Join("/", segments); + var port = uri.IsDefaultPort ? "" : $":{uri.Port}"; + return $"{uri.Host}{port}/{newPath}" + (string.IsNullOrEmpty(uri.Query) ? "" : $"{uri.Query}"); } #endregion } diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs index f76d56784..6ec95b2ed 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Yoma.Core.Domain.Core.Interfaces; using Yoma.Core.Domain.ShortLinkProvider.Interfaces; @@ -9,17 +8,14 @@ namespace Yoma.Core.Infrastructure.Bitly.Client public class BitlyClientFactory : IShortLinkProviderClientFactory { #region Class Variables - private readonly ILogger _logger; private readonly IEnvironmentProvider _environmentProvider; private readonly BitlyOptions _options; #endregion #region Constructor - public BitlyClientFactory(ILogger logger, - IEnvironmentProvider environmentProvider, + public BitlyClientFactory(IEnvironmentProvider environmentProvider, IOptions options) { - _logger = logger; _environmentProvider = environmentProvider; _options = options.Value; } @@ -28,7 +24,7 @@ public BitlyClientFactory(ILogger logger, #region Public Members public IShortLinkProviderClient CreateClient() { - return new BitlyClient(_logger, _environmentProvider, _options); + return new BitlyClient(_environmentProvider, _options); } #endregion } diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinksRequest.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkRequestCreate.cs similarity index 63% rename from src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinksRequest.cs rename to src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkRequestCreate.cs index 79c9dd943..fa76db490 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinksRequest.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkRequestCreate.cs @@ -2,7 +2,7 @@ namespace Yoma.Core.Infrastructure.Bitly.Models { - public class BitLinksRequest + public class BitLinkRequestCreate { [JsonProperty("long_url")] public string LongURL { get; set; } @@ -11,12 +11,17 @@ public class BitLinksRequest public string? Domain { get; set; } [JsonProperty("group_guid")] - public string GroupId { get; set; } + public string? GroupId { get; set; } [JsonProperty("title")] public string Title { get; set; } [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] - public string[]? Tags { get; set; } + public List? Tags { get; set; } + + public static implicit operator BitLinkRequestCreate(BitLinkResponse v) + { + throw new NotImplementedException(); + } } } diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkRequestPAtchCustomBack.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkRequestPAtchCustomBack.cs new file mode 100644 index 000000000..209af1614 --- /dev/null +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkRequestPAtchCustomBack.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Yoma.Core.Infrastructure.Bitly.Models +{ + public class BitLinkRequestPAtchCustomBack + { + [JsonProperty("bitlink_id")] + public string Id { get; set; } + + [JsonProperty("custom_bitlink")] + public string CustomLink { get; set; } + } +} diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinksResponse.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkResponse.cs similarity index 80% rename from src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinksResponse.cs rename to src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkResponse.cs index 3b0c3deac..61f8ad84e 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinksResponse.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkResponse.cs @@ -2,7 +2,7 @@ namespace Yoma.Core.Infrastructure.Bitly.Models { - public class BitLinksResponse + public class BitLinkResponse { [JsonProperty("link")] public string Link { get; set; } @@ -33,5 +33,11 @@ public class BitLinksResponse [JsonProperty("tags")] public string[] Tags { get; set; } + + [JsonProperty("launchpad_ids")] + public string[] LaunchpadIds { get; set; } + + [JsonProperty("campaign_ids")] + public string[] CampaignIds { get; set; } } } diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkResponseCustomBack.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkResponseCustomBack.cs new file mode 100644 index 000000000..bc25a76a1 --- /dev/null +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitLinkResponseCustomBack.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Yoma.Core.Infrastructure.Bitly.Models +{ + public class BitLinkResponseCustomBack + { + [JsonProperty("custom_bitlink")] + public string CustomBitLink { get; set; } + + [JsonProperty("bitlink")] + public BitLinkResponse BitLink { get; set; } + } +} diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs index f44767e59..61be03dd3 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs @@ -4,14 +4,14 @@ public class BitlyOptions { public const string Section = "Bitly"; + public string BaseUrl { get; set; } + public string GroupId { get; set; } public string ApiKey { get; set; } - public ShortLinkType ShortLinkType { get; set; } - - public string DomainCustom { get; set; } + public string GenericDomain { get; set; } - public string[] Tags { get; set; } + public string CustomDomain { get; set; } } } From 337a55c2bf89149aca685c4fe94fa53e00b47983 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:22:26 +0200 Subject: [PATCH 02/10] Linting --- .../Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs | 2 +- .../Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs b/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs index 535c53e71..31150c25b 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Core/Extensions/FlurlExtensions.cs @@ -67,7 +67,7 @@ public static async Task EnsureSuccessStatusCodeAsync(this Task< var message = await resp.ResponseMessage.Content.ReadAsStringAsync(); var successStatusCodes = AdditionalSuccessStatusCodes ?? []; - if(!successStatusCodes.Contains(HttpStatusCode.OK)) successStatusCodes.Add(HttpStatusCode.OK); + if (!successStatusCodes.Contains(HttpStatusCode.OK)) successStatusCodes.Add(HttpStatusCode.OK); if (successStatusCodes.Contains(statusCode)) return resp; throw new HttpClientException(statusCode, message); diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index 55f67e10c..6fea78b86 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -65,7 +65,7 @@ public async Task CreateShortLink(ShortLinkRequest request) .Append(_environmentProvider.Environment.ToString()) .Distinct(StringComparer.InvariantCultureIgnoreCase) .ToList(); - + var environmentPrefix = _environmentProvider.Environment != Domain.Core.Environment.Production ? $"{_environmentProvider.Environment}: " : string.Empty; var requestCreate = new BitLinkRequestCreate { @@ -104,7 +104,7 @@ private async Task UpdateCustomBackHalve(BitLinkResponse bitL { Id = bitLink.Id, Link = bitLink.Link - }; + }; switch (type) { From ad4ed075190fa6654b7c743f28403a0a25ad913b Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:24:43 +0200 Subject: [PATCH 03/10] Change method accessibility --- .../Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index 6fea78b86..13449884a 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -170,7 +170,7 @@ private async Task GenerateNewUriRelativeWithCustomBackHalf(string url, return result; } - public static string ReplaceLastSegmentAndRemoveScheme(string url, string newLastSegment) + private static string ReplaceLastSegmentAndRemoveScheme(string url, string newLastSegment) { var uri = new Uri(url, UriKind.RelativeOrAbsolute); var segments = uri.AbsolutePath.Trim('/').Split('/'); From 789c6265fb999f3f6fd55abffadbd0a993930be6 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:28:55 +0200 Subject: [PATCH 04/10] Linting --- .../Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index 13449884a..2b11cf9c0 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -20,7 +20,7 @@ public class BitlyClient : IShortLinkProviderClient private const string Header_Authorization = "Authorization"; private const string Header_Authorization_Value_Prefix = "Bearer"; - private readonly static string[] Common_Words = [ + private readonly static string[] Common_Words = { "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can", "cant", "cannot", "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", @@ -33,7 +33,8 @@ public class BitlyClient : IShortLinkProviderClient "theres", "these", "they", "theyd", "theyll", "theyre", "theyve", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasnt", "we", "wed", "well", "were", "weve", "werent", "what", "whats", "when", "whens", "where", "wheres", "which", "while", "who", "whos", "whom", "why", "whys", "will", "with", "wont", - "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves"]; + "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves" + }; #endregion #region Constructor From fc5ec0ce1ecbf2837fde3915216c7a0321ad3897 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:31:14 +0200 Subject: [PATCH 05/10] Linting --- .../Client/BitlyClient.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index 2b11cf9c0..e0627b0bd 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -20,21 +20,19 @@ public class BitlyClient : IShortLinkProviderClient private const string Header_Authorization = "Authorization"; private const string Header_Authorization_Value_Prefix = "Bearer"; - private readonly static string[] Common_Words = { - "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", - "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can", "cant", "cannot", - "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", - "for", "from", "further", "had", "hadnt", "has", "hasnt", "have", "havent", "having", "he", "hed", "hell", "hes", - "her", "here", "heres", "hers", "herself", "him", "himself", "his", "how", "hows", "i", "id", "ill", "im", "ive", - "if", "in", "into", "is", "isnt", "it", "its", "its", "itself", "lets", "me", "more", "most", "mustnt", "my", - "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", - "ourselves", "out", "over", "own", "same", "shant", "she", "shed", "shell", "shes", "should", "shouldnt", "so", - "some", "such", "than", "that", "thats", "the", "their", "theirs", "them", "themselves", "then", "there", - "theres", "these", "they", "theyd", "theyll", "theyre", "theyve", "this", "those", "through", "to", "too", - "under", "until", "up", "very", "was", "wasnt", "we", "wed", "well", "were", "weve", "werent", "what", "whats", - "when", "whens", "where", "wheres", "which", "while", "who", "whos", "whom", "why", "whys", "will", "with", "wont", - "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves" - }; + private readonly static string[] Common_Words = ["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", + "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can", "cant", "cannot", + "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", + "for", "from", "further", "had", "hadnt", "has", "hasnt", "have", "havent", "having", "he", "hed", "hell", "hes", + "her", "here", "heres", "hers", "herself", "him", "himself", "his", "how", "hows", "i", "id", "ill", "im", "ive", + "if", "in", "into", "is", "isnt", "it", "its", "its", "itself", "lets", "me", "more", "most", "mustnt", "my", + "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", + "ourselves", "out", "over", "own", "same", "shant", "she", "shed", "shell", "shes", "should", "shouldnt", "so", + "some", "such", "than", "that", "thats", "the", "their", "theirs", "them", "themselves", "then", "there", + "theres", "these", "they", "theyd", "theyll", "theyre", "theyve", "this", "those", "through", "to", "too", + "under", "until", "up", "very", "was", "wasnt", "we", "wed", "well", "were", "weve", "werent", "what", "whats", + "when", "whens", "where", "wheres", "which", "while", "who", "whos", "whom", "why", "whys", "will", "with", "wont", + "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves"]; #endregion #region Constructor From 14a2535a03fdde7f4ed520fa96b0668b55bf8ab4 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:33:56 +0200 Subject: [PATCH 06/10] Linting --- .../Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index e0627b0bd..be317f6f0 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -20,7 +20,7 @@ public class BitlyClient : IShortLinkProviderClient private const string Header_Authorization = "Authorization"; private const string Header_Authorization_Value_Prefix = "Bearer"; - private readonly static string[] Common_Words = ["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", + private readonly static string[] Common_Words = ["a","about","above","after","again","against","all","am","an","and","any","are","arent","as","at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can", "cant", "cannot", "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", "for", "from", "further", "had", "hadnt", "has", "hasnt", "have", "havent", "having", "he", "hed", "hell", "hes", From 221912d1b0ef45708c82d71ee9e048538b366a18 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:38:46 +0200 Subject: [PATCH 07/10] Linting --- .../Client/BitlyClient.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index be317f6f0..5c713295a 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -19,8 +19,9 @@ public class BitlyClient : IShortLinkProviderClient private const string Header_Authorization = "Authorization"; private const string Header_Authorization_Value_Prefix = "Bearer"; - - private readonly static string[] Common_Words = ["a","about","above","after","again","against","all","am","an","and","any","are","arent","as","at", + private const int MaximumRetryAttempts_GenerateUniqueURL = 10; + private readonly static string[] Common_Words = { + "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can", "cant", "cannot", "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", "for", "from", "further", "had", "hadnt", "has", "hasnt", "have", "havent", "having", "he", "hed", "hell", "hes", @@ -32,7 +33,8 @@ public class BitlyClient : IShortLinkProviderClient "theres", "these", "they", "theyd", "theyll", "theyre", "theyve", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasnt", "we", "wed", "well", "were", "weve", "werent", "what", "whats", "when", "whens", "where", "wheres", "which", "while", "who", "whos", "whom", "why", "whys", "will", "with", "wont", - "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves"]; + "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves" + }; #endregion #region Constructor @@ -142,7 +144,7 @@ private async Task GenerateNewUriRelativeWithCustomBackHalf(string url, var result = newUriRelative; var suffix = 1; - var maxAttempts = 10; + var maxAttempts = MaximumRetryAttempts_GenerateUniqueURL; do { try From ee871ee584d0fde88dfdc03cdfaa645acac9dcd6 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:41:17 +0200 Subject: [PATCH 08/10] Linting --- .../Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs index 61be03dd3..4674b20fe 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs @@ -4,7 +4,7 @@ public class BitlyOptions { public const string Section = "Bitly"; - public string BaseUrl { get; set; } + public string BaseUrl { get; set; } public string GroupId { get; set; } From bdd3a2d119ee25fcf180fcbf5a9784ea6005ffb2 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:01:09 +0200 Subject: [PATCH 09/10] Hooked up short link provider on opportunity sharing Added actions to get the opportunity sharing details --- .../Controllers/OpportunityController.cs | 35 +- .../Yoma.Core.Api/appsettings.json | 2 +- .../Core/Helpers/QRCodeHelper.cs | 19 + .../Core/Models/AppSettings.cs | 4 + .../Extensions/OpportunityExtensions.cs | 22 +- .../Interfaces/IOpportunityService.cs | 2 + .../Opportunity/Models/Opportunity.cs | 2 + .../Opportunity/Models/OpportunityInfo.cs | 2 + .../Models/OpportunitySharingResult.cs | 9 + .../Services/OpportunityInfoService.cs | 15 +- .../Services/OpportunityService.cs | 44 + .../ShortLinkProvider/Enumerations.cs | 26 +- .../Models/ShortLinkRequest.cs | 6 +- .../Yoma.Core.Domain/Yoma.Core.Domain.csproj | 5 +- .../Client/BitlyClient.cs | 149 +- .../Client/BitlyClientFactory.cs | 14 +- .../Models/BitlyOptions.cs | 2 - ...plicationDb_OpportunitySharing.Designer.cs | 2144 +++++++++++++++++ ...061727_ApplicationDb_OpportunitySharing.cs | 30 + .../ApplicationDbContextModelSnapshot.cs | 5 +- .../Opportunity/Entities/Opportunity.cs | 3 + .../Repositories/OpportunityRepository.cs | 5 + 22 files changed, 2400 insertions(+), 145 deletions(-) create mode 100644 src/api/src/domain/Yoma.Core.Domain/Core/Helpers/QRCodeHelper.cs create mode 100644 src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunitySharingResult.cs create mode 100644 src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.Designer.cs create mode 100644 src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs diff --git a/src/api/src/application/Yoma.Core.Api/Controllers/OpportunityController.cs b/src/api/src/application/Yoma.Core.Api/Controllers/OpportunityController.cs index 17289b9e9..d269e7075 100644 --- a/src/api/src/application/Yoma.Core.Api/Controllers/OpportunityController.cs +++ b/src/api/src/application/Yoma.Core.Api/Controllers/OpportunityController.cs @@ -66,6 +66,22 @@ public IActionResult GetPublishedOrExpiredById([FromRoute] Guid id) return StatusCode((int)HttpStatusCode.OK, result); } + [SwaggerOperation(Summary = "Get sharing details for published or expired opportunity by id (Anonymous)")] + [HttpGet("{id}/sharing")] + [ProducesResponseType(typeof(OpportunityInfo), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [AllowAnonymous] + public async Task GetSharingDetails([FromRoute] Guid id, [FromQuery] bool? includeQRCode) + { + _logger.LogInformation("Handling request {requestName}", nameof(GetSharingDetails)); + + var result = await _opportunityService.GetSharingDetails(id, true, includeQRCode); + + _logger.LogInformation("Request {requestName} handled", nameof(GetSharingDetails)); + + return StatusCode((int)HttpStatusCode.OK, result); + } + [SwaggerOperation(Summary = "Search for published opportunities based on the supplied filter (Anonymous)", Description = "Results are always associated with an active organization. By default published opportunities are included, thus active opportunities, irrespective of whether they started (includes both NotStarted and Active states). This default behavior is overridable")] [HttpPost("search")] @@ -204,13 +220,30 @@ public IActionResult GetInfoById([FromRoute] Guid id) { _logger.LogInformation("Handling request {requestName}", nameof(GetInfoById)); - //by default, all users possess the user role. Therefore, organizational authorization checks are omitted here, allowing org admins to access information for all opportunities without restriction. + //by default, all users possess the user role. Therefore, organizational authorization checks are omitted here, allowing org admins to access information for all opportunities without restriction var result = _opportunityInfoService.GetById(id, false); _logger.LogInformation("Request {requestName} handled", nameof(GetInfoById)); return StatusCode((int)HttpStatusCode.OK, result); } + + [SwaggerOperation(Summary = "Get sharing details for the specified opportunity by id (User, Admin or Organization Admin role required)")] + [HttpGet("{id}/auth/sharing")] + [ProducesResponseType(typeof(OpportunityInfo), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + [Authorize(Roles = $"{Constants.Role_User}, {Constants.Role_Admin}, {Constants.Role_OrganizationAdmin}")] + public async Task GetSharingDetailsAuthed([FromRoute] Guid id, [FromQuery] bool? includeQRCode) + { + _logger.LogInformation("Handling request {requestName}", nameof(GetSharingDetailsAuthed)); + + //by default, all users possess the user role. Therefore, organizational authorization checks are omitted here, allowing org admins to access information for all opportunities without restriction + var result = await _opportunityService.GetSharingDetails(id, false, includeQRCode); + + _logger.LogInformation("Request {requestName} handled", nameof(GetSharingDetailsAuthed)); + + return StatusCode((int)HttpStatusCode.OK, result); + } #endregion #region Administrative Actions diff --git a/src/api/src/application/Yoma.Core.Api/appsettings.json b/src/api/src/application/Yoma.Core.Api/appsettings.json index 71f381ccd..f28841cda 100644 --- a/src/api/src/application/Yoma.Core.Api/appsettings.json +++ b/src/api/src/application/Yoma.Core.Api/appsettings.json @@ -50,6 +50,7 @@ "SentryEnabledEnvironments": "Development, Staging, Production", "HttpsRedirectionEnabledEnvironments": null, "LaborMarketProviderAsSourceEnabledEnvironments": "Production", + "ShortLinkProviderAsSourceEnabledEnvironments": "Development, Staging, Production", "DatabaseRetryPolicy": { "MaxRetryCount": 6, "MaxRetryDelayInSeconds": 30 @@ -237,7 +238,6 @@ "BaseUrl": "https://api-ssl.bitly.com", "GroupId": "{groupid}", "ApiKey": "{apikey}", - "GenericDomain": "bit.ly", "CustomDomain": "go.yoma.world" } } diff --git a/src/api/src/domain/Yoma.Core.Domain/Core/Helpers/QRCodeHelper.cs b/src/api/src/domain/Yoma.Core.Domain/Core/Helpers/QRCodeHelper.cs new file mode 100644 index 000000000..199b66ff7 --- /dev/null +++ b/src/api/src/domain/Yoma.Core.Domain/Core/Helpers/QRCodeHelper.cs @@ -0,0 +1,19 @@ +using QRCoder; + +namespace Yoma.Core.Domain.Core.Helpers +{ + public static class QRCodeHelper + { + public static string GenerateQRCodeBase64(string url, QRCodeGenerator.ECCLevel ecc = QRCodeGenerator.ECCLevel.Q, int pixelsPerModule = 20) + { + using var qrGenerator = new QRCodeGenerator(); + var qrCodeData = qrGenerator.CreateQrCode(url, ecc); + + using var qrCode = new PngByteQRCode(qrCodeData); + var qrCodeAsBytes = qrCode.GetGraphic(pixelsPerModule); + + var base64String = Convert.ToBase64String(qrCodeAsBytes); + return $"data:image/png;base64,{base64String}"; + } + } +} diff --git a/src/api/src/domain/Yoma.Core.Domain/Core/Models/AppSettings.cs b/src/api/src/domain/Yoma.Core.Domain/Core/Models/AppSettings.cs index cc3174df7..b7c324ff0 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Core/Models/AppSettings.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Core/Models/AppSettings.cs @@ -87,6 +87,10 @@ public CacheItemType CacheEnabledByCacheItemTypesAsEnum public Environment LaborMarketProviderAsSourceEnabledEnvironmentsAsEnum => ParseEnvironmentInput(LaborMarketProviderAsSourceEnabledEnvironments); + public string ShortLinkProviderAsSourceEnabledEnvironments { get; set; } + + public Environment ShortLinkProviderAsSourceEnabledEnvironmentsAsEnum => ParseEnvironmentInput(ShortLinkProviderAsSourceEnabledEnvironments); + public AppSettingsDatabaseRetryPolicy DatabaseRetryPolicy { get; set; } public bool? RedisSSLCertificateValidationBypass { get; set; } diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Extensions/OpportunityExtensions.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Extensions/OpportunityExtensions.cs index 5ba4baa8e..4ce5af755 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Extensions/OpportunityExtensions.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Extensions/OpportunityExtensions.cs @@ -47,6 +47,20 @@ public static int TimeIntervalToDays(this Models.Opportunity opportunity) return days; } + public static (bool result, string? message) PublishedOrExpired(this Models.Opportunity opportunity) + { + ArgumentNullException.ThrowIfNull(opportunity, nameof(opportunity)); + + if (opportunity.OrganizationStatus != Entity.OrganizationStatus.Active) + return (false, $"Opportunity with id '{opportunity.Id}' belongs to an inactive organization"); + + var statuses = new List() { Status.Active, Status.Expired }; //ignore DateStart, includes both not started and started + if (!statuses.Contains(opportunity.Status)) + return (false, $"Opportunity with id '{opportunity.Id}' has an invalid status. Expected status(es): '{string.Join(", ", statuses)}'"); + + return (true, null); + } + public static void SetPublished(this Models.Opportunity opportunity) { ArgumentNullException.ThrowIfNull(opportunity, nameof(opportunity)); @@ -65,6 +79,11 @@ public static OpportunitySearchCriteriaItem ToOpportunitySearchCriteria(this Mod }; } + public static string YomaInfoURL(this Models.Opportunity value, string appBaseURL) + { + return appBaseURL.AppendPathSegment("opportunities").AppendPathSegment(value.Id).ToString(); + } + public static OpportunityInfo ToOpportunityInfo(this Models.Opportunity value, string appBaseURL) { ArgumentNullException.ThrowIfNull(value, nameof(value)); @@ -84,6 +103,7 @@ public static OpportunityInfo ToOpportunityInfo(this Models.Opportunity value, s Summary = value.Summary, Instructions = value.Instructions, URL = value.URL, + ShortURL = value.ShortURL, ZltoReward = value.ZltoReward, YomaReward = value.YomaReward, VerificationEnabled = value.VerificationEnabled, @@ -101,7 +121,7 @@ public static OpportunityInfo ToOpportunityInfo(this Models.Opportunity value, s DateStart = value.DateStart, DateEnd = value.DateEnd, Published = value.Published, - YomaInfoURL = appBaseURL.AppendPathSegment("opportunities").AppendPathSegment(value.Id).ToString(), + YomaInfoURL = value.YomaInfoURL(appBaseURL), Categories = value.Categories, Countries = value.Countries, Languages = value.Languages, diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Interfaces/IOpportunityService.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Interfaces/IOpportunityService.cs index f1295fe14..111dc7f0f 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Interfaces/IOpportunityService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Interfaces/IOpportunityService.cs @@ -11,6 +11,8 @@ public interface IOpportunityService Models.Opportunity? GetByTitleOrNull(string title, bool includeChildItems, bool includeComputed); + Task GetSharingDetails(Guid id, bool publishedOrExpiredOnly, bool? includeQRCode); + List Contains(string value, bool includeComputed); OpportunitySearchResultsCriteria SearchCriteriaOpportunities(OpportunitySearchFilterCriteria filter, bool ensureOrganizationAuthorization); diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/Opportunity.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/Opportunity.cs index b90f552c6..7054fdc9b 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/Opportunity.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/Opportunity.cs @@ -41,6 +41,8 @@ public class Opportunity public string? URL { get; set; } + public string? ShortURL { get; set; } + public decimal? ZltoReward { get; set; } public decimal? ZltoRewardPool { get; set; } diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunityInfo.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunityInfo.cs index a9551a82e..ed1a83d16 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunityInfo.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunityInfo.cs @@ -30,6 +30,8 @@ public class OpportunityInfo public string? URL { get; set; } + public string? ShortURL { get; set; } + [Name("Zlto Reward")] public decimal? ZltoReward { get; set; } diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunitySharingResult.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunitySharingResult.cs new file mode 100644 index 000000000..29eb8b1aa --- /dev/null +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Models/OpportunitySharingResult.cs @@ -0,0 +1,9 @@ +namespace Yoma.Core.Domain.Opportunity.Models +{ + public class OpportunitySharingResult + { + public string ShortURL { get; set; } + + public string? QRCodeBase64 { get; set; } + } +} diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityInfoService.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityInfoService.cs index 9cd487297..267b41e8d 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityInfoService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityInfoService.cs @@ -45,14 +45,13 @@ public OpportunityInfo GetPublishedOrExpiredById(Guid id) { var opportunity = _opportunityService.GetById(id, true, true, false); - //inactive organization - if (opportunity.OrganizationStatus != Entity.OrganizationStatus.Active) - throw new EntityNotFoundException($"Opportunity with id '{id}' belongs to an inactive organization"); - - //status criteria not met - var statuses = new List() { Status.Active, Status.Expired }; //ignore DateStart, includes both not started and started - if (!statuses.Contains(opportunity.Status)) - throw new EntityNotFoundException($"Opportunity with id '{id}' has an invalid status. Expected status(es): '{string.Join(", ", statuses)}'"); + var (publishedOrExpiredResult, message) = opportunity.PublishedOrExpired(); + + if (!publishedOrExpiredResult) + { + ArgumentException.ThrowIfNullOrEmpty(message); + throw new EntityNotFoundException(message); + } var result = opportunity.ToOpportunityInfo(_appSettings.AppBaseURL); SetParticipantCounts(result); diff --git a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityService.cs b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityService.cs index ff4d9e4fa..472eefaf4 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityService.cs +++ b/src/api/src/domain/Yoma.Core.Domain/Opportunity/Services/OpportunityService.cs @@ -25,6 +25,7 @@ using Yoma.Core.Domain.Opportunity.Interfaces.Lookups; using Yoma.Core.Domain.Opportunity.Models; using Yoma.Core.Domain.Opportunity.Validators; +using Yoma.Core.Domain.ShortLinkProvider.Interfaces; namespace Yoma.Core.Domain.Opportunity.Services { @@ -51,6 +52,7 @@ public class OpportunityService : IOpportunityService private readonly IEmailURLFactory _emailURLFactory; private readonly IEmailProviderClient _emailProviderClient; private readonly IIdentityProviderClient _identityProviderClient; + private readonly IShortLinkProviderClient _shortLinkProviderClient; private readonly OpportunityRequestValidatorCreate _opportunityRequestValidatorCreate; private readonly OpportunityRequestValidatorUpdate _opportunityRequestValidatorUpdate; @@ -94,6 +96,7 @@ public OpportunityService(ILogger logger, IEmailURLFactory emailURLFactory, IEmailProviderClientFactory emailProviderClientFactory, IIdentityProviderClientFactory identityProviderClientFactory, + IShortLinkProviderClientFactory shortLinkProviderClientFactory, OpportunityRequestValidatorCreate opportunityRequestValidatorCreate, OpportunityRequestValidatorUpdate opportunityRequestValidatorUpdate, OpportunitySearchFilterValidator opportunitySearchFilterValidator, @@ -126,6 +129,7 @@ public OpportunityService(ILogger logger, _emailURLFactory = emailURLFactory; _emailProviderClient = emailProviderClientFactory.CreateClient(); _identityProviderClient = identityProviderClientFactory.CreateClient(); + _shortLinkProviderClient = shortLinkProviderClientFactory.CreateClient(); _opportunityRequestValidatorCreate = opportunityRequestValidatorCreate; _opportunityRequestValidatorUpdate = opportunityRequestValidatorUpdate; @@ -194,6 +198,46 @@ public Models.Opportunity GetById(Guid id, bool includeChildItems, bool includeC return result; } + public async Task GetSharingDetails(Guid id, bool publishedOrExpiredOnly, bool? includeQRCode) + { + if (id == Guid.Empty) + throw new ArgumentNullException(nameof(id)); + + var opportunity = GetById(id, false, false, false); + + if (publishedOrExpiredOnly) + { + var (result, message) = opportunity.PublishedOrExpired(); + + if (!result) + { + ArgumentException.ThrowIfNullOrEmpty(message); + throw new EntityNotFoundException(message); + } + } + + if (string.IsNullOrEmpty(opportunity.ShortURL)) + { + var request = new ShortLinkProvider.Models.ShortLinkRequest + { + Type = ShortLinkProvider.EntityType.Opportunity, + Action = ShortLinkProvider.Action.Sharing, + Title = opportunity.Title, + URL = opportunity.YomaInfoURL(_appSettings.AppBaseURL) + }; + + var response = await _shortLinkProviderClient.CreateShortLink(request); + opportunity.ShortURL = response.Link; + await _opportunityRepository.Update(opportunity); + } + + return new OpportunitySharingResult + { + ShortURL = opportunity.ShortURL, + QRCodeBase64 = includeQRCode == true ? QRCodeHelper.GenerateQRCodeBase64(opportunity.ShortURL) : null + }; + } + public List Contains(string value, bool includeComputed) { if (string.IsNullOrWhiteSpace(value)) diff --git a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Enumerations.cs b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Enumerations.cs index 08a870615..f81007fe5 100644 --- a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Enumerations.cs +++ b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Enumerations.cs @@ -1,25 +1,13 @@ namespace Yoma.Core.Domain.ShortLinkProvider { - public enum ShortLinkType + public enum EntityType { - /// - /// Generic domain and no customized back-half - /// - Generic, - - /// - /// Custom domain and no customized back-half - /// - CustomDomain, - - /// - /// Generic domain and a customized back-half - /// - CustomBackHalf, + Opportunity + } - /// - /// Custom domain and a customized back-half - /// - CustomDomainAndBackHalf, + public enum Action + { + Sharing, + MagicLink //auto-verify } } diff --git a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs index ab7f61711..f4ddd2d1c 100644 --- a/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs +++ b/src/api/src/domain/Yoma.Core.Domain/ShortLinkProvider/Models/ShortLinkRequest.cs @@ -2,12 +2,14 @@ namespace Yoma.Core.Domain.ShortLinkProvider.Models { public class ShortLinkRequest { - public ShortLinkType Type { get; set; } + public EntityType Type { get; set; } + + public Action Action { get; set; } public string Title { get; set; } public string URL { get; set; } - public List? Tags { get; set; } + public List? ExtraTags { get; set; } } } diff --git a/src/api/src/domain/Yoma.Core.Domain/Yoma.Core.Domain.csproj b/src/api/src/domain/Yoma.Core.Domain/Yoma.Core.Domain.csproj index e0c046b77..ff15a8809 100644 --- a/src/api/src/domain/Yoma.Core.Domain/Yoma.Core.Domain.csproj +++ b/src/api/src/domain/Yoma.Core.Domain/Yoma.Core.Domain.csproj @@ -35,8 +35,9 @@ - - + + + diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs index 5c713295a..fb2d74094 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClient.cs @@ -1,10 +1,10 @@ using Flurl; using Flurl.Http; +using Microsoft.Extensions.Logging; using System.Net; -using Yoma.Core.Domain.Core.Exceptions; using Yoma.Core.Domain.Core.Extensions; using Yoma.Core.Domain.Core.Interfaces; -using Yoma.Core.Domain.ShortLinkProvider; +using Yoma.Core.Domain.Core.Models; using Yoma.Core.Domain.ShortLinkProvider.Interfaces; using Yoma.Core.Domain.ShortLinkProvider.Models; using Yoma.Core.Infrastructure.Bitly.Models; @@ -14,34 +14,24 @@ namespace Yoma.Core.Infrastructure.Bitly.Client public class BitlyClient : IShortLinkProviderClient { #region Class Variables + private readonly ILogger _logger; private readonly IEnvironmentProvider _environmentProvider; + private readonly AppSettings _appSettings; private readonly BitlyOptions _options; private const string Header_Authorization = "Authorization"; private const string Header_Authorization_Value_Prefix = "Bearer"; - private const int MaximumRetryAttempts_GenerateUniqueURL = 10; - private readonly static string[] Common_Words = { - "a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "arent", "as", "at", - "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can", "cant", "cannot", - "could", "couldnt", "did", "didnt", "do", "does", "doesnt", "doing", "dont", "down", "during", "each", "few", - "for", "from", "further", "had", "hadnt", "has", "hasnt", "have", "havent", "having", "he", "hed", "hell", "hes", - "her", "here", "heres", "hers", "herself", "him", "himself", "his", "how", "hows", "i", "id", "ill", "im", "ive", - "if", "in", "into", "is", "isnt", "it", "its", "its", "itself", "lets", "me", "more", "most", "mustnt", "my", - "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", - "ourselves", "out", "over", "own", "same", "shant", "she", "shed", "shell", "shes", "should", "shouldnt", "so", - "some", "such", "than", "that", "thats", "the", "their", "theirs", "them", "themselves", "then", "there", - "theres", "these", "they", "theyd", "theyll", "theyre", "theyve", "this", "those", "through", "to", "too", - "under", "until", "up", "very", "was", "wasnt", "we", "wed", "well", "were", "weve", "werent", "what", "whats", - "when", "whens", "where", "wheres", "which", "while", "who", "whos", "whom", "why", "whys", "will", "with", "wont", - "would", "wouldnt", "you", "youd", "youll", "youre", "youve", "your", "yours", "yourself", "yourselves" - }; #endregion #region Constructor - public BitlyClient(IEnvironmentProvider environmentProvider, - BitlyOptions options) + public BitlyClient(ILogger logger, + IEnvironmentProvider environmentProvider, + AppSettings appSettings, + BitlyOptions options) { + _logger = logger; _environmentProvider = environmentProvider; + _appSettings = appSettings; _options = options; } #endregion @@ -62,26 +52,29 @@ public async Task CreateShortLink(ShortLinkRequest request) if (!Uri.IsWellFormedUriString(request.URL, UriKind.Absolute)) throw new ArgumentException("Invalid URL", nameof(request)); - request.Tags = (request.Tags ?? []).Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()) - .Append(_environmentProvider.Environment.ToString()) - .Distinct(StringComparer.InvariantCultureIgnoreCase) - .ToList(); + if (!_appSettings.ShortLinkProviderAsSourceEnabledEnvironmentsAsEnum.HasFlag(_environmentProvider.Environment)) + { + _logger.LogInformation("Used dummy short link for environment '{environment}'", _environmentProvider.Environment); + return GenerateDummyShortLink(); + } + + var tags = (request.ExtraTags ?? []).Where(tag => !string.IsNullOrWhiteSpace(tag)).Select(tag => tag.Trim()).ToList(); + tags.Add(_environmentProvider.Environment.ToString()); + tags.Add("Yoma"); + tags.Add(request.Type.ToString()); + tags.Add(request.Action.ToString()); + tags = tags.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList(); - var environmentPrefix = _environmentProvider.Environment != Domain.Core.Environment.Production ? $"{_environmentProvider.Environment}: " : string.Empty; var requestCreate = new BitLinkRequestCreate { LongURL = request.URL, + Domain = _options.CustomDomain, GroupId = _options.GroupId, Title = request.Title, - Tags = request.Tags, - Domain = request.Type switch - { - ShortLinkType.Generic or ShortLinkType.CustomBackHalf => _options.GenericDomain, - ShortLinkType.CustomDomain or ShortLinkType.CustomDomainAndBackHalf => _options.CustomDomain, - _ => throw new ArgumentOutOfRangeException(nameof(request), request.Type, "Invalid type"), - } + Tags = tags, }; + //idempotent var response = await _options.BaseUrl .AppendPathSegment($"v4/bitlinks") .WithAuthHeader(GetAuthHeader()) @@ -89,7 +82,11 @@ public async Task CreateShortLink(ShortLinkRequest request) .EnsureSuccessStatusCodeAsync([HttpStatusCode.Created]) .ReceiveJson(); - return await UpdateCustomBackHalve(response, request.Type, request.Title); + return new ShortLinkResponse + { + Id = response.Id, + Link = response.Link + }; } #endregion @@ -99,86 +96,28 @@ private KeyValuePair GetAuthHeader() return new KeyValuePair(Header_Authorization, $"{Header_Authorization_Value_Prefix} {_options.ApiKey}"); } - private async Task UpdateCustomBackHalve(BitLinkResponse bitLink, ShortLinkType type, string title) + private ShortLinkResponse GenerateDummyShortLink() { - var result = new ShortLinkResponse - { - Id = bitLink.Id, - Link = bitLink.Link - }; + var path = GenerateRandomPath(6); + var urlRelative = $"{_options.CustomDomain}/{path}"; - switch (type) + return new ShortLinkResponse { - case ShortLinkType.CustomBackHalf: - case ShortLinkType.CustomDomainAndBackHalf: - var requestPatch = new BitLinkRequestPAtchCustomBack - { - Id = bitLink.Id, - CustomLink = await GenerateNewUriRelativeWithCustomBackHalf(result.Link, title) - - }; - - var response = await _options.BaseUrl - .AppendPathSegment($"v4/custom_bitlinks") - .WithAuthHeader(GetAuthHeader()) - .PostJsonAsync(requestPatch) - .EnsureSuccessStatusCodeAsync() - .ReceiveJson(); - - result.Id = response.BitLink.Id; - result.Link = response.CustomBitLink; - return result; - - default: - return result; - } + Id = urlRelative, + Link = $"https://{urlRelative}" + }; } - private async Task GenerateNewUriRelativeWithCustomBackHalf(string url, string title) + private static string GenerateRandomPath(int length) { - var titleWords = title.ToLower().Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); - var filteredWords = titleWords.Except(Common_Words).Distinct().Select(word => word.TitleCase()).ToList(); - var titleSquashed = filteredWords.Aggregate(string.Empty, (acc, word) => (acc.Length + word.Length <= 50) ? acc + word : acc); - - var newUriRelative = ReplaceLastSegmentAndRemoveScheme(url, titleSquashed); - var result = newUriRelative; + var random = new Random(); - var suffix = 1; - var maxAttempts = MaximumRetryAttempts_GenerateUniqueURL; - do - { - try - { - var resp = await _options.BaseUrl - .AppendPathSegment($"v4/bitlinks/{result}") - .WithAuthHeader(GetAuthHeader()) - .GetAsync() - .EnsureSuccessStatusCodeAsync(); - } - catch (HttpClientException ex) - { - if (ex.StatusCode == HttpStatusCode.NotFound) break; - } - - if (suffix >= maxAttempts) - throw new InvalidOperationException("Maximum attempts reached, unique URL cannot be generated"); - - result = $"{newUriRelative}_{suffix}"; - suffix++; - - } while (true); - - return result; - } + var chars = Enumerable.Range('0', '9' - '0' + 1).Select(i => (char)i) + .Concat(Enumerable.Range('A', 'Z' - 'A' + 1).Select(i => (char)i)) + .Concat(Enumerable.Range('a', 'z' - 'a' + 1).Select(i => (char)i)) + .ToArray(); - private static string ReplaceLastSegmentAndRemoveScheme(string url, string newLastSegment) - { - var uri = new Uri(url, UriKind.RelativeOrAbsolute); - var segments = uri.AbsolutePath.Trim('/').Split('/'); - segments[^1] = newLastSegment; - var newPath = string.Join("/", segments); - var port = uri.IsDefaultPort ? "" : $":{uri.Port}"; - return $"{uri.Host}{port}/{newPath}" + (string.IsNullOrEmpty(uri.Query) ? "" : $"{uri.Query}"); + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); } #endregion } diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs index 6ec95b2ed..5a2df554a 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Client/BitlyClientFactory.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Yoma.Core.Domain.Core.Interfaces; +using Yoma.Core.Domain.Core.Models; using Yoma.Core.Domain.ShortLinkProvider.Interfaces; using Yoma.Core.Infrastructure.Bitly.Models; @@ -8,15 +10,21 @@ namespace Yoma.Core.Infrastructure.Bitly.Client public class BitlyClientFactory : IShortLinkProviderClientFactory { #region Class Variables + private readonly ILogger _logger; private readonly IEnvironmentProvider _environmentProvider; + private readonly AppSettings _appSettings; private readonly BitlyOptions _options; #endregion #region Constructor - public BitlyClientFactory(IEnvironmentProvider environmentProvider, - IOptions options) + public BitlyClientFactory(ILogger logger, + IEnvironmentProvider environmentProvider, + IOptions appSettings, + IOptions options) { + _logger = logger; _environmentProvider = environmentProvider; + _appSettings = appSettings.Value; _options = options.Value; } #endregion @@ -24,7 +32,7 @@ public BitlyClientFactory(IEnvironmentProvider environmentProvider, #region Public Members public IShortLinkProviderClient CreateClient() { - return new BitlyClient(_environmentProvider, _options); + return new BitlyClient(_logger, _environmentProvider, _appSettings, _options); } #endregion } diff --git a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs index 4674b20fe..8b9cee66f 100644 --- a/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs +++ b/src/api/src/infrastructure/Yoam.Core.Infrastructure.Bitly/Models/BitlyOptions.cs @@ -10,8 +10,6 @@ public class BitlyOptions public string ApiKey { get; set; } - public string GenericDomain { get; set; } - public string CustomDomain { get; set; } } } diff --git a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.Designer.cs b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.Designer.cs new file mode 100644 index 000000000..9b063403d --- /dev/null +++ b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.Designer.cs @@ -0,0 +1,2144 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Yoma.Core.Infrastructure.Database.Context; + +#nullable disable + +namespace Yoma.Core.Infrastructure.Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240418061727_ApplicationDb_OpportunitySharing")] + partial class ApplicationDb_OpportunitySharing + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Core.Entities.BlobObject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("varchar(127)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("varchar(25)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("StorageType") + .IsRequired() + .HasColumnType("varchar(25)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.HasIndex("StorageType", "FileType", "ParentId"); + + b.ToTable("Blob", "Object"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.Lookups.OrganizationProviderType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OrganizationProviderType", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.Lookups.OrganizationStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OrganizationStatus", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Biography") + .HasColumnType("text"); + + b.Property("City") + .HasColumnType("varchar(50)"); + + b.Property("CommentApproval") + .HasColumnType("varchar(500)"); + + b.Property("CountryId") + .HasColumnType("uuid"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("DateStatusModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LogoId") + .HasColumnType("uuid"); + + b.Property("ModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("NameHashValue") + .IsRequired() + .HasColumnType("varchar(128)"); + + b.Property("PostalCode") + .HasColumnType("varchar(10)"); + + b.Property("PrimaryContactEmail") + .HasColumnType("varchar(320)"); + + b.Property("PrimaryContactName") + .HasColumnType("varchar(255)"); + + b.Property("PrimaryContactPhone") + .HasColumnType("varchar(50)"); + + b.Property("Province") + .HasColumnType("varchar(255)"); + + b.Property("RegistrationNumber") + .HasColumnType("varchar(255)"); + + b.Property("StatusId") + .HasColumnType("uuid"); + + b.Property("StreetAddress") + .HasColumnType("varchar(500)"); + + b.Property("Tagline") + .HasColumnType("text"); + + b.Property("TaxNumber") + .HasColumnType("varchar(255)"); + + b.Property("VATIN") + .HasColumnType("varchar(255)"); + + b.Property("WebsiteURL") + .HasColumnType("varchar(2048)"); + + b.HasKey("Id"); + + b.HasIndex("CountryId"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("LogoId"); + + b.HasIndex("ModifiedByUserId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("NameHashValue") + .IsUnique(); + + b.HasIndex("StatusId", "DateStatusModified", "DateCreated", "CreatedByUserId", "DateModified", "ModifiedByUserId"); + + b.ToTable("Organization", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.OrganizationDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("OrganizationId", "Type", "DateCreated"); + + b.ToTable("OrganizationDocuments", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.OrganizationProviderType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderTypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderTypeId"); + + b.HasIndex("OrganizationId", "ProviderTypeId") + .IsUnique(); + + b.ToTable("OrganizationProviderTypes", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.OrganizationUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique(); + + b.ToTable("OrganizationUsers", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CountryId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("DateOfBirth") + .HasColumnType("timestamp with time zone"); + + b.Property("DateYoIDOnboarded") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("EducationId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("varchar(320)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("ExternalId") + .HasColumnType("uuid"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.Property("GenderId") + .HasColumnType("uuid"); + + b.Property("PhoneNumber") + .HasColumnType("varchar(50)"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.Property("Surname") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.Property("YoIDOnboarded") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CountryId"); + + b.HasIndex("EducationId"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("GenderId"); + + b.HasIndex("PhotoId"); + + b.HasIndex("FirstName", "Surname", "EmailConfirmed", "PhoneNumber", "ExternalId", "YoIDOnboarded", "DateYoIDOnboarded", "DateCreated", "DateModified"); + + b.ToTable("User", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.UserSkill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SkillId"); + + b.HasIndex("UserId", "SkillId") + .IsUnique(); + + b.ToTable("UserSkills", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.UserSkillOrganization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserSkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserSkillId", "OrganizationId") + .IsUnique(); + + b.ToTable("UserSkillOrganizations", "Entity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Lookups.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CodeAlpha2") + .IsRequired() + .HasColumnType("varchar(2)"); + + b.Property("CodeAlpha3") + .IsRequired() + .HasColumnType("varchar(3)"); + + b.Property("CodeNumeric") + .IsRequired() + .HasColumnType("varchar(3)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.HasKey("Id"); + + b.HasIndex("CodeAlpha2") + .IsUnique(); + + b.HasIndex("CodeAlpha3") + .IsUnique(); + + b.HasIndex("CodeNumeric") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Country", "Lookup"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Lookups.Entities.Education", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Education", "Lookup"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Lookups.Entities.Gender", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Gender", "Lookup"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Lookups.Entities.Language", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CodeAlpha2") + .IsRequired() + .HasColumnType("varchar(2)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.HasKey("Id"); + + b.HasIndex("CodeAlpha2") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Language", "Lookup"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Lookups.Entities.Skill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .IsRequired() + .HasColumnType("varchar(100)"); + + b.Property("InfoURL") + .HasColumnType("varchar(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("ExternalId") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Skill", "Lookup"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Lookups.Entities.TimeInterval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("TimeInterval", "Lookup"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Marketplace.Entities.Lookups.TransactionStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("TransactionStatus", "Marketplace"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Marketplace.Entities.TransactionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(8,2)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemCategoryId") + .IsRequired() + .HasColumnType("varchar(50)"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("varchar(50)"); + + b.Property("StatusId") + .HasColumnType("uuid"); + + b.Property("TransactionId") + .IsRequired() + .HasColumnType("varchar(50)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("StatusId"); + + b.HasIndex("UserId", "ItemCategoryId", "ItemId", "StatusId", "DateCreated", "DateModified"); + + b.ToTable("TransactionLog", "Marketplace"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.Lookups.MyOpportunityAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MyOpportunityAction", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.Lookups.MyOpportunityVerificationStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MyOpportunityVerificationStatus", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionId") + .HasColumnType("uuid"); + + b.Property("CommentVerification") + .HasColumnType("varchar(500)"); + + b.Property("DateCompleted") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("DateStart") + .HasColumnType("timestamp with time zone"); + + b.Property("OpportunityId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerificationStatusId") + .HasColumnType("uuid"); + + b.Property("YomaReward") + .HasColumnType("decimal(8,2)"); + + b.Property("ZltoReward") + .HasColumnType("decimal(8,2)"); + + b.HasKey("Id"); + + b.HasIndex("ActionId"); + + b.HasIndex("OpportunityId"); + + b.HasIndex("UserId", "OpportunityId", "ActionId") + .IsUnique(); + + b.HasIndex("VerificationStatusId", "DateCompleted", "ZltoReward", "YomaReward", "DateCreated", "DateModified"); + + b.ToTable("MyOpportunity", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunityVerification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("GeometryProperties") + .HasColumnType("text"); + + b.Property("MyOpportunityId") + .HasColumnType("uuid"); + + b.Property("VerificationTypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("VerificationTypeId"); + + b.HasIndex("MyOpportunityId", "VerificationTypeId") + .IsUnique(); + + b.ToTable("MyOpportunityVerifications", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageURL") + .IsRequired() + .HasColumnType("varchar(2048)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpportunityCategory", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityDifficulty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpportunityDifficulty", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpportunityStatus", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpportunityType", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityVerificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpportunityVerificationType", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommitmentIntervalCount") + .HasColumnType("smallint"); + + b.Property("CommitmentIntervalId") + .HasColumnType("uuid"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("CredentialIssuanceEnabled") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("DateStart") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DifficultyId") + .HasColumnType("uuid"); + + b.Property("Instructions") + .HasColumnType("text"); + + b.Property("Keywords") + .HasColumnType("varchar(500)"); + + b.Property("ModifiedByUserId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ParticipantCount") + .HasColumnType("integer"); + + b.Property("ParticipantLimit") + .HasColumnType("integer"); + + b.Property("SSISchemaName") + .HasColumnType("varchar(255)"); + + b.Property("ShortURL") + .HasColumnType("varchar(2048)"); + + b.Property("StatusId") + .HasColumnType("uuid"); + + b.Property("Summary") + .HasColumnType("varchar(500)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TypeId") + .HasColumnType("uuid"); + + b.Property("URL") + .HasColumnType("varchar(2048)"); + + b.Property("VerificationEnabled") + .HasColumnType("boolean"); + + b.Property("VerificationMethod") + .HasColumnType("varchar(20)"); + + b.Property("YomaReward") + .HasColumnType("decimal(8,2)"); + + b.Property("YomaRewardCumulative") + .HasColumnType("decimal(12,2)"); + + b.Property("YomaRewardPool") + .HasColumnType("decimal(12,2)"); + + b.Property("ZltoReward") + .HasColumnType("decimal(8,2)"); + + b.Property("ZltoRewardCumulative") + .HasColumnType("decimal(12,2)"); + + b.Property("ZltoRewardPool") + .HasColumnType("decimal(12,2)"); + + b.HasKey("Id"); + + b.HasIndex("CommitmentIntervalId"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("Description") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Description"), "GIN"); + + b.HasIndex("DifficultyId"); + + b.HasIndex("ModifiedByUserId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("StatusId"); + + b.HasIndex("Title") + .IsUnique(); + + b.HasIndex("TypeId", "OrganizationId", "ZltoReward", "DifficultyId", "CommitmentIntervalId", "CommitmentIntervalCount", "StatusId", "Keywords", "DateStart", "DateEnd", "CredentialIssuanceEnabled", "DateCreated", "CreatedByUserId", "DateModified", "ModifiedByUserId"); + + b.ToTable("Opportunity", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("OpportunityId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("OpportunityId", "CategoryId") + .IsUnique(); + + b.ToTable("OpportunityCategories", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityCountry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CountryId") + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("OpportunityId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CountryId"); + + b.HasIndex("OpportunityId", "CountryId") + .IsUnique(); + + b.ToTable("OpportunityCountries", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityLanguage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("LanguageId") + .HasColumnType("uuid"); + + b.Property("OpportunityId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("LanguageId"); + + b.HasIndex("OpportunityId", "LanguageId") + .IsUnique(); + + b.ToTable("OpportunityLanguages", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunitySkill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("OpportunityId") + .HasColumnType("uuid"); + + b.Property("SkillId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SkillId"); + + b.HasIndex("OpportunityId", "SkillId") + .IsUnique(); + + b.ToTable("OpportunitySkills", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityVerificationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("varchar(255)"); + + b.Property("OpportunityId") + .HasColumnType("uuid"); + + b.Property("VerificationTypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("VerificationTypeId"); + + b.HasIndex("OpportunityId", "VerificationTypeId") + .IsUnique(); + + b.ToTable("OpportunityVerificationTypes", "Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Reward.Entities.Lookups.RewardTransactionStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(30)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_TransactionStatus_Name1"); + + b.ToTable("TransactionStatus", "Reward"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Reward.Entities.Lookups.WalletCreationStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("WalletCreationStatus", "Reward"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Reward.Entities.RewardTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(8,2)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorReason") + .HasColumnType("text"); + + b.Property("MyOpportunityId") + .HasColumnType("uuid"); + + b.Property("RetryCount") + .HasColumnType("smallint"); + + b.Property("SourceEntityType") + .IsRequired() + .HasColumnType("varchar(25)"); + + b.Property("StatusId") + .HasColumnType("uuid"); + + b.Property("TransactionId") + .HasColumnType("varchar(50)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MyOpportunityId"); + + b.HasIndex("StatusId", "DateCreated", "DateModified"); + + b.HasIndex("UserId", "SourceEntityType", "MyOpportunityId") + .IsUnique(); + + b.ToTable("Transaction", "Reward"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Reward.Entities.WalletCreation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Balance") + .HasColumnType("decimal(12,2)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorReason") + .HasColumnType("text"); + + b.Property("RetryCount") + .HasColumnType("smallint"); + + b.Property("StatusId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("WalletId") + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("StatusId", "DateCreated", "DateModified"); + + b.ToTable("WalletCreation", "Reward"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSICredentialIssuanceStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("CredentialIssuanceStatus", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("TypeName") + .IsUnique(); + + b.ToTable("SchemaEntity", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntityProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.Property("Format") + .HasColumnType("varchar(125)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(50)"); + + b.Property("NameDisplay") + .IsRequired() + .HasColumnType("varchar(50)"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.Property("SSISchemaEntityId") + .HasColumnType("uuid"); + + b.Property("SystemType") + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("SSISchemaEntityId", "Name") + .IsUnique(); + + b.ToTable("SchemaEntityProperty", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntityType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("SSISchemaEntityId") + .HasColumnType("uuid"); + + b.Property("SSISchemaTypeId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SSISchemaTypeId"); + + b.HasIndex("SSISchemaEntityId", "SSISchemaTypeId") + .IsUnique(); + + b.ToTable("SchemaEntityType", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.Property("SupportMultiple") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("SchemaType", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSITenantCreationStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("TenantCreationStatus", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.SSICredentialIssuance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ArtifactType") + .IsRequired() + .HasColumnType("varchar(25)"); + + b.Property("CredentialId") + .HasColumnType("varchar(50)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorReason") + .HasColumnType("text"); + + b.Property("MyOpportunityId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RetryCount") + .HasColumnType("smallint"); + + b.Property("SchemaName") + .IsRequired() + .HasColumnType("varchar(125)"); + + b.Property("SchemaTypeId") + .HasColumnType("uuid"); + + b.Property("SchemaVersion") + .IsRequired() + .HasColumnType("varchar(20)"); + + b.Property("StatusId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MyOpportunityId"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("StatusId"); + + b.HasIndex("UserId"); + + b.HasIndex("SchemaName", "UserId", "OrganizationId", "MyOpportunityId") + .IsUnique(); + + b.HasIndex("SchemaTypeId", "ArtifactType", "SchemaName", "StatusId", "DateCreated", "DateModified"); + + b.ToTable("CredentialIssuance", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.SSITenantCreation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("varchar(25)"); + + b.Property("ErrorReason") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RetryCount") + .HasColumnType("smallint"); + + b.Property("StatusId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("varchar(50)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("EntityType", "UserId", "OrganizationId") + .IsUnique(); + + b.HasIndex("StatusId", "DateCreated", "DateModified"); + + b.ToTable("TenantCreation", "SSI"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Core.Entities.BlobObject", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Core.Entities.BlobObject", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Country", "Country") + .WithMany() + .HasForeignKey("CountryId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Core.Entities.BlobObject", "Logo") + .WithMany() + .HasForeignKey("LogoId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "ModifiedByUser") + .WithMany() + .HasForeignKey("ModifiedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Lookups.OrganizationStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Country"); + + b.Navigation("CreatedByUser"); + + b.Navigation("Logo"); + + b.Navigation("ModifiedByUser"); + + b.Navigation("Status"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.OrganizationDocument", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Core.Entities.BlobObject", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", "Organization") + .WithMany("Documents") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.OrganizationProviderType", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", "Organization") + .WithMany("ProviderTypes") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Lookups.OrganizationProviderType", "ProviderType") + .WithMany() + .HasForeignKey("ProviderTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ProviderType"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.OrganizationUser", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", "Organization") + .WithMany("Administrators") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.User", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Country", "Country") + .WithMany() + .HasForeignKey("CountryId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Education", "Education") + .WithMany() + .HasForeignKey("EducationId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Gender", "Gender") + .WithMany() + .HasForeignKey("GenderId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.Core.Entities.BlobObject", "Photo") + .WithMany() + .HasForeignKey("PhotoId"); + + b.Navigation("Country"); + + b.Navigation("Education"); + + b.Navigation("Gender"); + + b.Navigation("Photo"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.UserSkill", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany("Skills") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Skill"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.UserSkillOrganization", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.UserSkill", "UserSkill") + .WithMany("Organizations") + .HasForeignKey("UserSkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("UserSkill"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Marketplace.Entities.TransactionLog", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Marketplace.Entities.Lookups.TransactionStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Status"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunity", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.Lookups.MyOpportunityAction", "Action") + .WithMany() + .HasForeignKey("ActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", "Opportunity") + .WithMany() + .HasForeignKey("OpportunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.Lookups.MyOpportunityVerificationStatus", "VerificationStatus") + .WithMany() + .HasForeignKey("VerificationStatusId"); + + b.Navigation("Action"); + + b.Navigation("Opportunity"); + + b.Navigation("User"); + + b.Navigation("VerificationStatus"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunityVerification", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Core.Entities.BlobObject", "File") + .WithMany() + .HasForeignKey("FileId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunity", "MyOpportunity") + .WithMany("Verifications") + .HasForeignKey("MyOpportunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityVerificationType", "VerificationType") + .WithMany() + .HasForeignKey("VerificationTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("MyOpportunity"); + + b.Navigation("VerificationType"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.TimeInterval", "CommitmentInterval") + .WithMany() + .HasForeignKey("CommitmentIntervalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityDifficulty", "Difficulty") + .WithMany() + .HasForeignKey("DifficultyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "ModifiedByUser") + .WithMany() + .HasForeignKey("ModifiedByUserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CommitmentInterval"); + + b.Navigation("CreatedByUser"); + + b.Navigation("Difficulty"); + + b.Navigation("ModifiedByUser"); + + b.Navigation("Organization"); + + b.Navigation("Status"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityCategory", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityCategory", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", "Opportunity") + .WithMany("Categories") + .HasForeignKey("OpportunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityCountry", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Country", "Country") + .WithMany() + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", "Opportunity") + .WithMany("Countries") + .HasForeignKey("OpportunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Country"); + + b.Navigation("Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityLanguage", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Language", "Language") + .WithMany() + .HasForeignKey("LanguageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", "Opportunity") + .WithMany("Languages") + .HasForeignKey("OpportunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Language"); + + b.Navigation("Opportunity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunitySkill", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", "Opportunity") + .WithMany("Skills") + .HasForeignKey("OpportunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Lookups.Entities.Skill", "Skill") + .WithMany() + .HasForeignKey("SkillId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Opportunity"); + + b.Navigation("Skill"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.OpportunityVerificationType", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", "Opportunity") + .WithMany("VerificationTypes") + .HasForeignKey("OpportunityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Lookups.OpportunityVerificationType", "VerificationType") + .WithMany() + .HasForeignKey("VerificationTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Opportunity"); + + b.Navigation("VerificationType"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Reward.Entities.RewardTransaction", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunity", "MyOpportunity") + .WithMany() + .HasForeignKey("MyOpportunityId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.Reward.Entities.Lookups.RewardTransactionStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MyOpportunity"); + + b.Navigation("Status"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Reward.Entities.WalletCreation", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Reward.Entities.Lookups.WalletCreationStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Status"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntityProperty", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntity", "SSISchemaEntity") + .WithMany("Properties") + .HasForeignKey("SSISchemaEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SSISchemaEntity"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntityType", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntity", "SSISchemaEntity") + .WithMany("Types") + .HasForeignKey("SSISchemaEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaType", "SSISchemaType") + .WithMany() + .HasForeignKey("SSISchemaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SSISchemaEntity"); + + b.Navigation("SSISchemaType"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.SSICredentialIssuance", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunity", "MyOpportunity") + .WithMany() + .HasForeignKey("MyOpportunityId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaType", "SchemaType") + .WithMany() + .HasForeignKey("SchemaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSICredentialIssuanceStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("MyOpportunity"); + + b.Navigation("Organization"); + + b.Navigation("SchemaType"); + + b.Navigation("Status"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.SSITenantCreation", b => + { + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSITenantCreationStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Yoma.Core.Infrastructure.Database.Entity.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Status"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.Organization", b => + { + b.Navigation("Administrators"); + + b.Navigation("Documents"); + + b.Navigation("ProviderTypes"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.User", b => + { + b.Navigation("Skills"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Entity.Entities.UserSkill", b => + { + b.Navigation("Organizations"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.MyOpportunity.Entities.MyOpportunity", b => + { + b.Navigation("Verifications"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.Opportunity.Entities.Opportunity", b => + { + b.Navigation("Categories"); + + b.Navigation("Countries"); + + b.Navigation("Languages"); + + b.Navigation("Skills"); + + b.Navigation("VerificationTypes"); + }); + + modelBuilder.Entity("Yoma.Core.Infrastructure.Database.SSI.Entities.Lookups.SSISchemaEntity", b => + { + b.Navigation("Properties"); + + b.Navigation("Types"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs new file mode 100644 index 000000000..6e0ad84b7 --- /dev/null +++ b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Yoma.Core.Infrastructure.Database.Migrations +{ + /// + public partial class ApplicationDb_OpportunitySharing : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShortURL", + schema: "Opportunity", + table: "Opportunity", + type: "varchar(2048)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShortURL", + schema: "Opportunity", + table: "Opportunity"); + } + } +} diff --git a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/ApplicationDbContextModelSnapshot.cs b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/ApplicationDbContextModelSnapshot.cs index 4c2ff3965..a501333a7 100644 --- a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("ProductVersion", "8.0.3") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -953,6 +953,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SSISchemaName") .HasColumnType("varchar(255)"); + b.Property("ShortURL") + .HasColumnType("varchar(2048)"); + b.Property("StatusId") .HasColumnType("uuid"); diff --git a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs index 87b18f253..5007559f1 100644 --- a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs +++ b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs @@ -45,6 +45,9 @@ public class Opportunity : BaseEntity [Column(TypeName = "varchar(2048)")] public string? URL { get; set; } + [Column(TypeName = "varchar(2048)")] + public string? ShortURL { get; set; } + [Column(TypeName = "decimal(8,2)")] public decimal? ZltoReward { get; set; } diff --git a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Repositories/OpportunityRepository.cs b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Repositories/OpportunityRepository.cs index eceac4991..20352af12 100644 --- a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Repositories/OpportunityRepository.cs +++ b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Repositories/OpportunityRepository.cs @@ -42,6 +42,7 @@ public OpportunityRepository(ApplicationDbContext context) : base(context) { } Summary = entity.Summary, Instructions = entity.Instructions, URL = entity.URL, + ShortURL = entity.ShortURL, ZltoReward = entity.ZltoReward, YomaReward = entity.YomaReward, ZltoRewardPool = entity.ZltoRewardPool, @@ -143,6 +144,7 @@ public OpportunityRepository(ApplicationDbContext context) : base(context) { } Summary = item.Summary, Instructions = item.Instructions, URL = item.URL, + ShortURL = item.ShortURL, ZltoReward = item.ZltoReward, YomaReward = item.YomaReward, ZltoRewardPool = item.ZltoRewardPool, @@ -191,6 +193,7 @@ public OpportunityRepository(ApplicationDbContext context) : base(context) { } Summary = item.Summary, Instructions = item.Instructions, URL = item.URL, + ShortURL = item.ShortURL, ZltoReward = item.ZltoReward, YomaReward = item.YomaReward, ZltoRewardPool = item.ZltoRewardPool, @@ -244,6 +247,7 @@ public OpportunityRepository(ApplicationDbContext context) : base(context) { } entity.Summary = item.Summary; entity.Instructions = item.Instructions; entity.URL = item.URL; + entity.ShortURL = item.ShortURL; entity.ZltoReward = item.ZltoReward; entity.YomaReward = item.YomaReward; entity.ZltoRewardPool = item.ZltoRewardPool; @@ -292,6 +296,7 @@ public OpportunityRepository(ApplicationDbContext context) : base(context) { } entity.Summary = item.Summary; entity.Instructions = item.Instructions; entity.URL = item.URL; + entity.ShortURL = item.ShortURL; entity.ZltoReward = item.ZltoReward; entity.YomaReward = item.YomaReward; entity.ZltoRewardPool = item.ZltoRewardPool; From 2d4c34331fbd118c35366798616c9964786eb249 Mon Sep 17 00:00:00 2001 From: adrianwium <82496337+adrianwium@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:07:16 +0200 Subject: [PATCH 10/10] Linting --- ...061727_ApplicationDb_OpportunitySharing.cs | 40 +++++++++---------- .../Opportunity/Entities/Opportunity.cs | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs index 6e0ad84b7..dc6ad9bec 100644 --- a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs +++ b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Migrations/20240418061727_ApplicationDb_OpportunitySharing.cs @@ -1,30 +1,30 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Yoma.Core.Infrastructure.Database.Migrations { + /// + public partial class ApplicationDb_OpportunitySharing : Migration + { /// - public partial class ApplicationDb_OpportunitySharing : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "ShortURL", - schema: "Opportunity", - table: "Opportunity", - type: "varchar(2048)", - nullable: true); - } + migrationBuilder.AddColumn( + name: "ShortURL", + schema: "Opportunity", + table: "Opportunity", + type: "varchar(2048)", + nullable: true); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "ShortURL", - schema: "Opportunity", - table: "Opportunity"); - } + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ShortURL", + schema: "Opportunity", + table: "Opportunity"); } + } } diff --git a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs index 5007559f1..9d28317aa 100644 --- a/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs +++ b/src/api/src/infrastructure/Yoma.Core.Infrastructure.Database/Opportunity/Entities/Opportunity.cs @@ -46,7 +46,7 @@ public class Opportunity : BaseEntity public string? URL { get; set; } [Column(TypeName = "varchar(2048)")] - public string? ShortURL { get; set; } + public string? ShortURL { get; set; } [Column(TypeName = "decimal(8,2)")] public decimal? ZltoReward { get; set; }