Skip to content

Commit

Permalink
Merge pull request #66 from IliyanIlievPH/65
Browse files Browse the repository at this point in the history
Implement voucher presents
  • Loading branch information
starkmsu authored Jun 15, 2020
2 parents c462363 + 2578c02 commit 9f4d3c2
Show file tree
Hide file tree
Showing 18 changed files with 280 additions and 6 deletions.
7 changes: 7 additions & 0 deletions client/MAVN.Service.SmartVouchers.Client/ISmartVouchersApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,12 @@ public interface ISmartVouchersApi
/// <param name="pageData">Page data.</param>
[Get("/api/vouchers/bycampaign")]
Task<PaginatedVouchersListResponseModel> GetCampaignVouchersAsync(Guid campaignId, [Query] BasePaginationRequestModel pageData);

/// <summary>
/// Present vouchers to customers.
/// </summary>
/// <param name="request">The model that describes vouchers present request.</param>
[Post("/api/vouchers/present")]
Task<PresentVouchersResponse> PresentVouchersAsync([Body] PresentVouchersRequest request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace MAVN.Service.SmartVouchers.Client.Models.Requests
{
/// <summary>
/// Request model to present vouchers
/// </summary>
public class PresentVouchersRequest
{
/// <summary>
/// Id of the campaign
/// </summary>
[Required]
public Guid CampaignId { get; set; }

/// <summary>
/// Id of the admin
/// </summary>
[Required]
public Guid AdminId { get; set; }

/// <summary>
/// Emails of customer receivers
/// </summary>
[Required]
public List<string> CustomerEmails { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace MAVN.Service.SmartVouchers.Client.Models.Responses.Enums
{
/// <summary>
/// Error codes
/// </summary>
public enum PresentVouchersErrorCodes
{
/// <summary>
/// no error
/// </summary>
None,
/// <summary>
/// Voucher campaign is missing
/// </summary>
VoucherCampaignNotFound,
/// <summary>
/// Voucher campaign is not active
/// </summary>
VoucherCampaignNotActive,
/// <summary>
/// Not enough vouchers in stock
/// </summary>
NotEnoughVouchersInStock,
/// <summary>
/// Admin user is not the creator of the campaign
/// </summary>
IncorrectAdminUser,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using MAVN.Service.SmartVouchers.Client.Models.Responses.Enums;

namespace MAVN.Service.SmartVouchers.Client.Models.Responses
{
/// <summary>
/// response model
/// </summary>
public class PresentVouchersResponse
{
/// <summary>
/// error
/// </summary>
public PresentVouchersErrorCodes Error { get; set; }
/// <summary>
/// Not registered emails
/// </summary>
public List<string> NotRegisteredEmails { get; set; }
}
}
5 changes: 4 additions & 1 deletion settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ PaymentManagementServiceClient:
settings-key: PaymentManagementServiceUrl
PartnerManagementServiceClient:
ServiceUrl:
settings-key: PartnerManagementServiceUrl
settings-key: PartnerManagementServiceUrl
CustomerProfileServiceClient:
ServiceUrl:
settings-key: CustomerProfileServiceUrl
2 changes: 2 additions & 0 deletions src/MAVN.Job.SmartVouchers/Modules/ServiceModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Lykke.SettingsReader;
using MAVN.Job.SmartVouchers.Services;
using MAVN.Job.SmartVouchers.Settings;
using MAVN.Service.CustomerProfile.Client;
using MAVN.Service.PartnerManagement.Client;
using MAVN.Service.PaymentManagement.Client;
using MAVN.Service.SmartVouchers.Domain.Services;
Expand Down Expand Up @@ -55,6 +56,7 @@ protected override void Load(ContainerBuilder builder)

builder.RegisterPaymentManagementClient(_settings.PaymentManagementServiceClient, null);
builder.RegisterPartnerManagementClient(_settings.PartnerManagementServiceClient, null);
builder.RegisterCustomerProfileClient(_settings.CustomerProfileServiceClient, null);
}
}
}
3 changes: 3 additions & 0 deletions src/MAVN.Job.SmartVouchers/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Lykke.Sdk.Settings;
using MAVN.Job.SmartVouchers.Settings.JobSettings;
using MAVN.Service.CustomerProfile.Client;
using MAVN.Service.PartnerManagement.Client;
using MAVN.Service.PaymentManagement.Client;

Expand All @@ -12,5 +13,7 @@ public class AppSettings : BaseAppSettings
public PaymentManagementServiceClientSettings PaymentManagementServiceClient { get; set; }

public PartnerManagementServiceClientSettings PartnerManagementServiceClient { get; set; }

public CustomerProfileServiceClientSettings CustomerProfileServiceClient { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace MAVN.Service.SmartVouchers.Domain.Enums
{
public enum PresentVouchersErrorCodes
{
None,
VoucherCampaignNotFound,
VoucherCampaignNotActive,
NotEnoughVouchersInStock,
IncorrectAdminUser,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Lykke.Common" Version="7.5.0" />
<PackageReference Include="MAVN.Service.CustomerProfile.Client" Version="2.0.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using MAVN.Service.SmartVouchers.Domain.Enums;

namespace MAVN.Service.SmartVouchers.Domain.Models
{
public class PresentVouchersResult
{
public PresentVouchersErrorCodes Error { get; set; }
public List<string> NotRegisteredEmails { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ Task<VouchersPage> GetByOwnerIdAsync(
Task<List<Voucher>> GetReservedVouchersBeforeDateAsync(DateTime reservationTimeoutDate);
Task SetVouchersFromCampaignsAsExpired(Guid[] campaignsIds);
Task<bool> AnyReservedVouchersAsync(Guid customerId);
Task<int> GetReservedVouchersCountForCampaign(Guid campaignId);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MAVN.Service.SmartVouchers.Domain.Enums;
using MAVN.Service.SmartVouchers.Domain.Models;
Expand All @@ -23,5 +24,6 @@ Task ProcessStuckReservedVouchersAsync(TimeSpan generatePaymentTimeoutPeriod,
TimeSpan finishPaymentTimeoutPeriod);

Task MarkVouchersFromExpiredCampaignsAsExpired();
Task<PresentVouchersResult> PresentVouchersAsync(Guid campaignId, Guid adminId, List<string> customerEmails);
}
}
133 changes: 129 additions & 4 deletions src/MAVN.Service.SmartVouchers.DomainServices/VouchersService.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Common.Log;
using Lykke.Common.Log;
using Lykke.RabbitMqBroker.Publisher;
using MAVN.Service.CustomerProfile.Client;
using MAVN.Service.CustomerProfile.Client.Models.Requests;
using MAVN.Service.PartnerManagement.Client;
using MAVN.Service.PaymentManagement.Client;
using MAVN.Service.PaymentManagement.Client.Models.Requests;
Expand All @@ -26,6 +29,7 @@ public class VouchersService : IVouchersService

private readonly IPaymentManagementClient _paymentManagementClient;
private readonly IPartnerManagementClient _partnerManagementClient;
private readonly ICustomerProfileClient _customerProfileClient;
private readonly IVouchersRepository _vouchersRepository;
private readonly ICampaignsRepository _campaignsRepository;
private readonly IPaymentRequestsRepository _paymentRequestsRepository;
Expand All @@ -39,6 +43,7 @@ public class VouchersService : IVouchersService
public VouchersService(
IPaymentManagementClient paymentManagementClient,
IPartnerManagementClient partnerManagementClient,
ICustomerProfileClient customerProfileClient,
IVouchersRepository vouchersRepository,
ICampaignsRepository campaignsRepository,
IPaymentRequestsRepository paymentRequestsRepository,
Expand All @@ -51,6 +56,7 @@ public VouchersService(
{
_paymentManagementClient = paymentManagementClient;
_partnerManagementClient = partnerManagementClient;
_customerProfileClient = customerProfileClient;
_vouchersRepository = vouchersRepository;
_campaignsRepository = campaignsRepository;
_paymentRequestsRepository = paymentRequestsRepository;
Expand Down Expand Up @@ -102,8 +108,6 @@ public async Task<VoucherReservationResult> ReserveVoucherAsync(Guid voucherCamp
if (campaign.VouchersTotalCount <= campaign.BoughtVouchersCount)
return new VoucherReservationResult { ErrorCode = ProcessingVoucherError.NoAvailableVouchers };



var voucherPriceIsZero = campaign.VoucherPrice == 0;
var voucherCampaignIdStr = voucherCampaignId.ToString();
for (int i = 0; i < MaxAttemptsCount; ++i)
Expand Down Expand Up @@ -216,6 +220,127 @@ public async Task<VoucherReservationResult> ReserveVoucherAsync(Guid voucherCamp
return new VoucherReservationResult { ErrorCode = ProcessingVoucherError.NoAvailableVouchers };
}

public async Task<PresentVouchersResult> PresentVouchersAsync(Guid campaignId, Guid adminId, List<string> customerEmails)
{
var campaign = await _campaignsRepository.GetByIdAsync(campaignId, false);
if (campaign == null)
return new PresentVouchersResult { Error = PresentVouchersErrorCodes.VoucherCampaignNotFound };

if (campaign.State != CampaignState.Published
|| DateTime.UtcNow < campaign.FromDate
|| (campaign.ToDate.HasValue && campaign.ToDate.Value < DateTime.UtcNow))
return new PresentVouchersResult { Error = PresentVouchersErrorCodes.VoucherCampaignNotActive };

if (campaign.VouchersTotalCount <= campaign.BoughtVouchersCount)
return new PresentVouchersResult { Error = PresentVouchersErrorCodes.NotEnoughVouchersInStock };

if (campaign.CreatedBy != adminId.ToString())
return new PresentVouchersResult { Error = PresentVouchersErrorCodes.IncorrectAdminUser };

var (customerIds, notRegisteredEmails) = await GetCustomerIdsByEmails(customerEmails);
var voucherCampaignIdStr = campaignId.ToString();
var adminIdStr = adminId.ToString();
for (var i = 0; i < MaxAttemptsCount; ++i)
{
var locked = await _redisLocksService.TryAcquireLockAsync(
voucherCampaignIdStr,
adminIdStr,
_lockTimeOut);

if (!locked)
{
await Task.Delay(_lockTimeOut);
continue;
}

var reservedVouchersCount = await _vouchersRepository.GetReservedVouchersCountForCampaign(campaignId);
var availableVouchersCount = campaign.VouchersTotalCount - campaign.BoughtVouchersCount - reservedVouchersCount;
if (availableVouchersCount < customerIds.Count)
{
await _redisLocksService.ReleaseLockAsync(voucherCampaignIdStr, adminIdStr);
return new PresentVouchersResult { Error = PresentVouchersErrorCodes.NotEnoughVouchersInStock };
}

var vouchers = await _vouchersRepository.GetByCampaignIdAndStatusAsync(campaignId, VoucherStatus.InStock);
foreach (var customerId in customerIds)
{
Voucher voucher = null;
if (vouchers.Any())
{
voucher = vouchers.First();

voucher.Status = VoucherStatus.Sold;
voucher.OwnerId = customerId;
voucher.PurchaseDate = DateTime.UtcNow;
await _vouchersRepository.UpdateAsync(voucher);
vouchers.Remove(voucher);
}
else
{
var validationCode = GenerateValidation();
voucher = new Voucher
{
CampaignId = campaignId,
Status = VoucherStatus.Sold,
OwnerId = customerId,
PurchaseDate = DateTime.UtcNow,
};

voucher.Id = await _vouchersRepository.CreateAsync(voucher);
voucher.ShortCode = GenerateShortCodeFromId(voucher.Id);

await _vouchersRepository.UpdateAsync(voucher, validationCode);
}

await _voucherSoldPublisher.PublishAsync(new SmartVoucherSoldEvent
{
CampaignId = campaignId,
PartnerId = campaign.PartnerId,
Amount = 0,
Currency = campaign.Currency,
CustomerId = customerId,
Timestamp = DateTime.UtcNow,
VoucherShortCode = voucher.ShortCode,
});
}

await _redisLocksService.ReleaseLockAsync(voucherCampaignIdStr, adminIdStr);

return new PresentVouchersResult
{
Error = PresentVouchersErrorCodes.None,
NotRegisteredEmails = notRegisteredEmails.ToList(),
};
}

_log.Warning($"Couldn't get a lock when trying to present vouchers for voucher campaign {campaign}");

throw new Exception($"Couldn't get a lock when trying to present vouchers for voucher campaign {campaign}");
}

private async Task<(HashSet<Guid> CustomerIds, HashSet<string> NotRegisteredEmails)> GetCustomerIdsByEmails(List<string> customerEmails)
{
var customerIds = new HashSet<Guid>();
var notRegisteredEmails = new HashSet<string>();

foreach (var customerEmail in customerEmails)
{
var profile = await _customerProfileClient.CustomerProfiles.GetByEmailAsync(new GetByEmailRequestModel
{
Email = customerEmail,
IncludeNotVerified = true,
IncludeNotActive = false
});

if (profile.Profile != null)
customerIds.Add(Guid.Parse(profile.Profile.CustomerId));
else
notRegisteredEmails.Add(customerEmail);
}

return (customerIds, notRegisteredEmails);
}

public async Task<ProcessingVoucherError> CancelVoucherReservationAsync(string shortCode)
{
var voucher = await _vouchersRepository.GetByShortCodeAsync(shortCode);
Expand Down Expand Up @@ -270,7 +395,7 @@ public async Task<RedeemVoucherError> RedeemVoucherAsync(string voucherShortCode
if (!linkedPartner.HasValue)
return RedeemVoucherError.SellerCustomerIsNotALinkedPartner;

if(linkedPartner.Value != campaign.PartnerId)
if (linkedPartner.Value != campaign.PartnerId)
return RedeemVoucherError.SellerCustomerIsNotTheVoucherIssuer;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,5 +230,16 @@ public async Task<bool> AnyReservedVouchersAsync(Guid customerId)
return result;
}
}

public async Task<int> GetReservedVouchersCountForCampaign(Guid campaignId)
{
using (var context = _contextFactory.CreateDataContext())
{
var result = await context.Vouchers
.CountAsync(x => x.CampaignId == campaignId && x.Status == VoucherStatus.Reserved);

return result;
}
}
}
}
1 change: 1 addition & 0 deletions src/MAVN.Service.SmartVouchers/AutoMapperProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public AutoMapperProfile()
.ForMember(e => e.Language, opt => opt.Ignore());

CreateMap<VoucherReservationResult, ReserveVoucherResponse>(MemberList.Destination);
CreateMap<PresentVouchersResult, PresentVouchersResponse>();
}
}
}
Loading

0 comments on commit 9f4d3c2

Please sign in to comment.