Skip to content

Commit

Permalink
feat(QueueRentalAgreementRequest): Implement rental agreement request…
Browse files Browse the repository at this point in the history
… queuing with AWS SQS integration

- Add AwsSQSMessageBroker project for message queuing via AWS SQS.
- Update init-localstack.sh to include rental agreement request queues for LocalStack setup.
- Introduce new API endpoints in DeliveryDriverHttpApiAdapter for queuing rental agreement requests.
- Expand application core with use cases and validators for rental agreement request processing.
- Adjust DeliveryDriverRepository to support new rental management functionalities.
- Add unit tests for rental agreement request validation and execution.

This commit introduces the capability for delivery drivers to queue rental agreement requests, leveraging AWS SQS for asynchronous message processing. The changes include updates to the application core, adapters, and unit tests to ensure functionality and maintainability.

#10
  • Loading branch information
chariondm committed Mar 24, 2024
1 parent a7ad194 commit 05cec79
Show file tree
Hide file tree
Showing 28 changed files with 845 additions and 54 deletions.
7 changes: 7 additions & 0 deletions MotoDeliveryManager.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsS3StorageAdapter", "src\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQSDriverLicensePhotoProcessorAdapter", "src\Adapters\Inbound\SQSDriverLicensePhotoProcessorAdapter\SQSDriverLicensePhotoProcessorAdapter.csproj", "{99B95C76-8749-404E-B768-85C3F6DCB66A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsSQSMessageBroker", "src\Adapters\Outbounds\AwsSQSMessageBroker\AwsSQSMessageBroker.csproj", "{9B07D003-F10B-42AA-9E6F-0485A977953C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -82,6 +84,10 @@ Global
{99B95C76-8749-404E-B768-85C3F6DCB66A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99B95C76-8749-404E-B768-85C3F6DCB66A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99B95C76-8749-404E-B768-85C3F6DCB66A}.Release|Any CPU.Build.0 = Release|Any CPU
{9B07D003-F10B-42AA-9E6F-0485A977953C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9B07D003-F10B-42AA-9E6F-0485A977953C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9B07D003-F10B-42AA-9E6F-0485A977953C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B07D003-F10B-42AA-9E6F-0485A977953C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{76B78ADA-7B17-408E-BA0F-197149CBBF8D} = {4E2F7D63-D3C8-4ED5-8FC8-96D93A413391}
Expand All @@ -99,5 +105,6 @@ Global
{4E7CED8C-A104-4F72-B72B-1CC3AA75FC25} = {E964C640-3EE4-4C20-8778-F5EFFC39B661}
{CC8E2307-AF36-43DB-9D20-6698A80F5A4E} = {E85EE701-5FF2-4F11-8161-5656A3D9555D}
{99B95C76-8749-404E-B768-85C3F6DCB66A} = {E964C640-3EE4-4C20-8778-F5EFFC39B661}
{9B07D003-F10B-42AA-9E6F-0485A977953C} = {E85EE701-5FF2-4F11-8161-5656A3D9555D}
EndGlobalSection
EndGlobal
67 changes: 29 additions & 38 deletions docker-setup/init-localstack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@
S3_BUCKET_NAME=${1:-delivery-driver-licenses-photo}
SQS_QUEUE_NAME=${2:-delivery-driver-license-queue}
DLQ_QUEUE_NAME=${3:-delivery-driver-license-dlq}

# The URL for accessing LocalStack, defaulting to http://localstack:4566.
# This is useful for environments where LocalStack is accessed through a different port or host.
SQS_RENTAL_AGREEMENT_REQUEST_QUEUE_NAME=${4:-delivery-driver-rental-agreement-request-queue}
DLQ_RENTAL_AGREEMENT_REQUEST_QUEUE_NAME=${5:-delivery-driver-rental-agreement-request-dlq}
LOCALSTACK_URL="http://localstack:4566"

# Waits for LocalStack to be fully ready by repeatedly checking its health endpoint.
# It specifically waits for the S3 service to be reported as "available" before proceeding.
wait_for_localstack() {
echo "Waiting for LocalStack to be ready..."
while ! curl -s $LOCALSTACK_URL/_localstack/health | grep -q "\"s3\": \"available\""; do
while ! curl -s "$LOCALSTACK_URL"/_localstack/health | grep -q "\"s3\": \"available\""; do
echo "LocalStack is not ready yet; trying again in 1 second..."
sleep 1
done
Expand All @@ -27,22 +26,22 @@ wait_for_localstack() {
# Creates an S3 bucket with the name specified by the S3_BUCKET_NAME variable.
# The bucket is created using the AWS CLI, pointed at the LocalStack endpoint.
create_s3_bucket() {
aws --endpoint-url=$LOCALSTACK_URL s3 mb s3://$S3_BUCKET_NAME
echo "S3 bucket created: $S3_BUCKET_NAME"
aws --endpoint-url="$LOCALSTACK_URL" s3 mb s3://"$1"
echo "S3 bucket created: $1"
}

# Sets a bucket policy on the newly created S3 bucket.
# The policy allows "PutObject" actions for files matching "*.bmp" and "*.png" extensions, from any principal.
set_bucket_policy() {
POLICY=$(cat <<EOF
local policy=$(cat <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowOnlyBmpAndPngFiles",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::$S3_BUCKET_NAME/*",
"Resource": "arn:aws:s3:::$1/*",
"Condition": {
"StringLike": {
"s3:objectKey": [
Expand All @@ -56,41 +55,33 @@ set_bucket_policy() {
EOF
)

aws --endpoint-url=$LOCALSTACK_URL s3api put-bucket-policy --bucket $S3_BUCKET_NAME --policy "$POLICY"
echo "Bucket policy set for $S3_BUCKET_NAME."
aws --endpoint-url="$LOCALSTACK_URL" s3api put-bucket-policy --bucket "$1" --policy "$policy"
echo "Bucket policy set for $1."
}

# Creates an SQS queue with the name specified by the SQS_QUEUE_NAME variable.
# The queue URL is outputted to the console for reference.
# Creates an SQS queue with the name specified by the queue_name argument.
# Also creates a Dead Letter Queue (DLQ) with the name specified by the dlq_name argument.
# The DLQ is associated with the main queue, allowing messages that fail to be processed to be sent to the DLQ.
# The DLQ is created with a redrive policy that sends messages to the DLQ after a single receive attempt.
create_sqs_queue() {
QUEUE_URL=$(aws --endpoint-url=$LOCALSTACK_URL sqs create-queue --queue-name $SQS_QUEUE_NAME --query 'QueueUrl' --output text)
echo "SQS Queue created: $SQS_QUEUE_NAME"
}

# Creates a Dead Letter Queue (DLQ) with the name specified by the DLQ_QUEUE_NAME variable.
# It then associates this DLQ with the primary SQS queue, setting the redrive policy to redirect messages
# that have been received more than 3 times without successful processing.
create_dlq() {
DLQ_URL=$(aws --endpoint-url=$LOCALSTACK_URL sqs create-queue --queue-name $DLQ_QUEUE_NAME --query 'QueueUrl' --output text)
echo "DLQ created: $DLQ_QUEUE_NAME"

DLQ_ARN=$(aws --endpoint-url=$LOCALSTACK_URL sqs get-queue-attributes --queue-url $DLQ_URL --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)

# Use o caminho absoluto do arquivo JSON dentro do container.
JSON_PATH="/docker-entrypoint-initaws.d/set-queue-attributes.json"
local queue_url=$(aws --endpoint-url="$LOCALSTACK_URL" sqs create-queue --queue-name "$1" --query 'QueueUrl' --output text)
echo "SQS Queue created: $1"
echo "$queue_url"

# Substitui o placeholder no arquivo JSON pelos valores reais usando o caminho absoluto.
sed -i "s|\[DLQ_ARN\]|$DLQ_ARN|g" $JSON_PATH
local dlq_url=$(aws --endpoint-url="$LOCALSTACK_URL" sqs create-queue --queue-name "$2" --query 'QueueUrl' --output text)
echo "SQS DLQ Queue created: $2"
echo "$dlq_url"

local dlq_arn=$(aws --endpoint-url="$LOCALSTACK_URL" sqs get-queue-attributes --queue-url "$dlq_url" --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
local json_path="/docker-entrypoint-initaws.d/set-queue-attributes.json"

# Usa o caminho absoluto no comando AWS CLI.
aws --endpoint-url=$LOCALSTACK_URL sqs set-queue-attributes --queue-url $QUEUE_URL --attributes "file://$JSON_PATH"
echo "DLQ set for SQS Queue: $SQS_QUEUE_NAME"
aws --endpoint-url="$LOCALSTACK_URL" sqs set-queue-attributes --queue-url "$dlq_url" --attributes "file://$json_path"
echo "DLQ $2 associated with SQS Queue $1"
}

# The main execution flow of the script, calling the functions defined above in order to
# set up the LocalStack environment with the specified resources.
# Main script logic
wait_for_localstack
create_s3_bucket
set_bucket_policy
create_sqs_queue
create_dlq
create_s3_bucket "$S3_BUCKET_NAME"
set_bucket_policy "$S3_BUCKET_NAME"
create_sqs_queue "$SQS_QUEUE_NAME" "$DLQ_QUEUE_NAME"
create_sqs_queue "$SQS_RENTAL_AGREEMENT_REQUEST_QUEUE_NAME" "$DLQ_RENTAL_AGREEMENT_REQUEST_QUEUE_NAME"
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace MotoDeliveryManager.Adapters.Inbound.DeliveryDriverHttpApiAdapter.Controllers.Rentals.QueueRentalAgreementRequest.V1;

/// <summary>
/// Represents the request to queue a rental agreement.
/// </summary>
/// <param name="DeliveryDriverId">The identifier of the delivery driver.</param>
/// <param name="RentalPlanId">The identifier of the motorcycle rental plan.</param>
/// <param name="ExpectedReturnDate">The expected return date of the motorcycle.</param>
/// <remarks>It is used to request a rental agreement for a motorcycle.</remarks>
public record QueueRentalAgreementRequestRequest(
[Required]
Guid DeliveryDriverId,
[Required]
Guid RentalPlanId,
[Required]
[DataType(DataType.Date)]
DateOnly ExpectedReturnDate);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace MotoDeliveryManager.Adapters.Inbound.DeliveryDriverHttpApiAdapter.Controllers.Rentals.QueueRentalAgreementRequest.V1;

/// <summary>
/// Represents the response for a queued rental agreement request.
/// </summary>
/// <param name="RentalAgreementId"></param>
/// <remarks>It is used to describe the outcome of a rental agreement request.</remarks>
public record QueueRentalAgreementRequestResponse(Guid RentalAgreementId);
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace MotoDeliveryManager.Adapters.Inbound.DeliveryDriverHttpApiAdapter.Controllers.Rentals.QueueRentalAgreementRequest.V1;

/// <summary>
/// Represents the controller for the rental management endpoints.
/// </summary>
/// <remarks>It is used to queue a rental agreement request for a motorcycle.</remarks>
/// <seealso cref="IQueueRentalAgreementRequestOutcomeHandler"/>
/// <seealso cref="IQueueRentalAgreementRequestUseCase"/>
[ApiController]
[Route("api/v1/rentals")]
[Produces("application/json")]
[Consumes("application/json")]
public sealed class RentalController(ILogger<RentalController> logger)
: ControllerBase, IQueueRentalAgreementRequestOutcomeHandler
{
private readonly ILogger<RentalController> _logger = logger;

private IResult? _viewModel;

void IQueueRentalAgreementRequestOutcomeHandler.RentalAgreementRequestNotQueued(IDictionary<string, string[]> errors)
{
var message = "The rental agreement request was not queued.";
var response = ApiResponse<ProblemDetails>.CreateValidationError(errors, HttpContext, message);
_viewModel = Results.UnprocessableEntity(response);
}

void IQueueRentalAgreementRequestOutcomeHandler.RentalAgreementRequestNotValid(IDictionary<string, string[]> errors)
{
var message = "The rental agreement request was not valid.";
var response = ApiResponse<ProblemDetails>.CreateValidationError(errors, HttpContext, message);
_viewModel = Results.BadRequest(response);
}

void IQueueRentalAgreementRequestOutcomeHandler.RentalAgreementRequestQueued(Guid rentalAgreementId)
{
var message = "The rental agreement request was successfully queued.";
var rentalAgreementResponse = new QueueRentalAgreementRequestResponse(rentalAgreementId);
var response = ApiResponse<QueueRentalAgreementRequestResponse>.CreateSuccess(rentalAgreementResponse, message);
_viewModel = Results.Accepted(string.Empty, response);
}

/// <summary>
/// Queues a rental agreement request.
/// </summary>
/// <param name="request">The request to queue a rental agreement.</param>
/// <param name="useCase">The use case to queue a rental agreement request.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The result of the rental agreement request queuing.</returns>
/// <response code="202">The rental agreement request was successfully queued.</response>
/// <response code="400">The rental agreement request was not valid.</response>
/// <response code="422">The rental agreement request was not queued.</response>
/// <remarks>It is used to queue a rental agreement request for a motorcycle.</remarks>
/// <seealso cref="QueueRentalAgreementRequestRequest"/>
/// <seealso cref="QueueRentalAgreementRequestResponse"/>
/// <seealso cref="IQueueRentalAgreementRequestUseCase"/>
/// <seealso cref="IQueueRentalAgreementRequestOutcomeHandler"/>
/// <example>
/// POST /api/v1/rentals/agreements/queue
/// {
/// "deliveryDriverId": "b3f3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b",
/// "rentalPlanId": "b3f3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b",
/// "expectedReturnDate: "2022-12-31"
/// }
/// </example>
[HttpPost("agreements/queue", Name = "QueueRentalAgreementRequest")]
[ProducesResponseType(typeof(ApiResponse<QueueRentalAgreementRequestResponse>), StatusCodes.Status202Accepted)]
[ProducesResponseType(typeof(ApiResponse<ProblemDetails>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiResponse<ProblemDetails>), StatusCodes.Status422UnprocessableEntity)]
public async Task<IResult> QueueRentalAgreementRequestAsync(
[FromBody] QueueRentalAgreementRequestRequest request,
[FromKeyedServices(UseCaseType.Validation)] IQueueRentalAgreementRequestUseCase useCase,
CancellationToken cancellationToken)
{
useCase.SetOutcomeHandler(this);

var inbound = new QueueRentalAgreementRequestInbound(
Guid.NewGuid(),
request.DeliveryDriverId,
request.RentalPlanId,
request.ExpectedReturnDate);

await useCase.ExecuteAsync(inbound, cancellationToken);

return _viewModel!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ProjectReference Include="..\..\..\Core\Application\Application.csproj" />
<ProjectReference Include="..\..\Outbounds\AwsS3StorageAdapter\AwsS3StorageAdapter.csproj" />
<ProjectReference Include="..\..\Outbounds\PostgresDbAdapter\PostgresDbAdapter.csproj" />
<ProjectReference Include="..\..\Outbounds\AwsSQSMessageBroker\AwsSQSMessageBroker.csproj" />
</ItemGroup>

<ItemGroup>
Expand All @@ -30,13 +31,16 @@
<Using Include="MotoDeliveryManager.Adapters.Inbound.DeliveryDriverHttpApiAdapter.Modules.Common" />
<Using Include="MotoDeliveryManager.Adapters.Inbound.DeliveryDriverHttpApiAdapter.Modules.Common.Swagger" />
<Using Include="MotoDeliveryManager.Adapters.Outbounds.AwsS3StorageAdapter" />
<Using Include="MotoDeliveryManager.Adapters.Outbounds.AwsSQSMessageBroker" />
<Using Include="MotoDeliveryManager.Adapters.Outbounds.PostgresDbAdapter.Entities.DeliveryDrivers" />
<Using Include="MotoDeliveryManager.Adapters.Outbounds.PostgresDbAdapter.Entities.Rentals" />
<Using Include="MotoDeliveryManager.Core.Application.Common" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.DeliveryDrivers.RegisterDeliveryDriver" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.DeliveryDrivers.RegisterDeliveryDriver.Inbounds" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.Rentals.ListRentalPlans" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.Rentals.ListRentalPlans.Inbounds" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.Rentals.QueueRentalAgreementRequest" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.Rentals.QueueRentalAgreementRequest.Inbounds" />
<Using Include="MotoDeliveryManager.Core.Domain.DeliveryDrivers" />
<Using Include="MotoDeliveryManager.Core.Domain.Rentals" />
<Using Include="System.ComponentModel.DataAnnotations" />
Expand Down
6 changes: 4 additions & 2 deletions src/Adapters/Inbound/DeliveryDriverHttpApiAdapter/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
.AddPostgresDbAdapterRentalPlanRepository(builder.Configuration.GetConnectionString("DefaultConnection")!);

builder.Services
.AddAwsS3StorageAdapter(builder.Configuration);
.AddAwsS3StorageAdapter(builder.Configuration)
.AddSqsMessageProducer(builder.Configuration);

builder.Services
.AddRegisterDeliveryDriverUseCase()
.AddListRentalPlansUseCase();
.AddListRentalPlansUseCase()
.AddQueueRentalAgreementRequestUseCase();

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>MotoDeliveryManager.Adapters.Outbounds.AwsSQSMessageBroker</AssemblyName>
<Description>The AwsS3StorageAdapter project is responsible for the implementation of the outbound ports of the application using the AWS SQS message broker.</Description>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<RootNamespace>MotoDeliveryManager.Adapters.Outbounds.AwsSQSMessageBroker</RootNamespace>
<TargetFramework>net8.0</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.300" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.60" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Core\Application\Application.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Amazon.SQS" />
<Using Include="Amazon.SQS.Model" />
<Using Include="Microsoft.Extensions.Configuration" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Logging" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.Rentals.QueueRentalAgreementRequest.Inbounds" />
<Using Include="MotoDeliveryManager.Core.Application.UseCases.Rentals.QueueRentalAgreementRequest.Outbounds" />
<Using Include="System.Text.Json" />
</ItemGroup>

</Project>
48 changes: 48 additions & 0 deletions src/Adapters/Outbounds/AwsSQSMessageBroker/SqsMessageProducer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace MotoDeliveryManager.Adapters.Outbounds.AwsSQSMessageBroker;

public class SqsMessageProducer(IAmazonSQS sqsClient, string queueUrl, ILogger<SqsMessageProducer> logger)
: IQueueRentalAgreementRequestMessageBroker
{
private const string SuccessMessage = "Rental Agreement Request with Id: {RentalAgreementRequestId} has been queued successfully. Message Id: {MessageId}, Queue Url: {QueueUrl}";

private const string ErrorMessage = "An error occurred while sending a message to SQS queue: {QueueUrl}. AWS Error Code: {ErrorCode}, Error Message: {ErrorMessage}";

private const string UnexpectedErrorMessage = "An unexpected error occurred while sending a message to SQS queue: {QueueUrl}";

private readonly IAmazonSQS _sqsClient = sqsClient;

private readonly string _queueUrl = queueUrl;

public readonly ILogger<SqsMessageProducer> _logger = logger;

public async Task QueueRentalAgreementRequestAsync(
QueueRentalAgreementRequestInbound rentalAgreementRequest,
CancellationToken cancellationToken)
{
try
{
var messageBody = JsonSerializer.Serialize(rentalAgreementRequest);

var sendMessageRequest = new SendMessageRequest
{
QueueUrl = _queueUrl,
MessageBody = messageBody
};

var response = await _sqsClient.SendMessageAsync(sendMessageRequest);

_logger.LogInformation(SuccessMessage, rentalAgreementRequest.RentalAgreementId, response.MessageId, _queueUrl);
}
catch (AmazonSQSException ex)
{
_logger.LogError(ex, ErrorMessage, _queueUrl, ex.ErrorCode, ex.Message);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, UnexpectedErrorMessage, _queueUrl);
throw;
}
}
}

Loading

0 comments on commit 05cec79

Please sign in to comment.