Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.CrossCutting/ServiceConfiguration/DevelopmentSeamExtensions.cs
T
2026-07-28 22:25:15 +03:30

59 lines
3.3 KiB
C#

using Baya.Application.Contracts.Common;
using Baya.Infrastructure.CrossCutting.Seams;
using Microsoft.Extensions.DependencyInjection;
namespace Baya.Infrastructure.CrossCutting.ServiceConfiguration;
public static class DevelopmentSeamExtensions
{
/// <summary>
/// Development-only wiring for the OTP bring-up bridge. Registers <see cref="DevOtpStore"/> and decorates
/// the <see cref="ISmsSender"/> registered by <c>AddCrossCuttingSeams</c> with
/// <see cref="DevCapturingSmsSender"/>, so each OTP is also captured in memory for
/// <c>/api/v1/dev/last_otp/{phone}</c> to serve. MUST be called only inside
/// <c>builder.Environment.IsDevelopment()</c>, and only for a capture-safe provider (the log-only
/// <see cref="LoggingSmsSender"/> or the Development-only Telegram relay — <c>Program.cs</c> owns that
/// condition): nothing here is wired in any other environment, which — together with the endpoint's own
/// <c>IsDevelopment()</c> guard — makes the OTP echo impossible to enable outside Development.
/// </summary>
public static IServiceCollection AddDevelopmentOtpCapture(this IServiceCollection services)
{
services.AddSingleton<DevOtpStore>();
// Decorate whatever ISmsSender is already registered rather than assuming the mock — with
// Seams:Sms:Provider = telegram the inner sender is TelegramSmsSender, and re-creating a LoggingSmsSender
// here would silently swallow the delivery instead of capturing alongside it. Last registration wins for
// a single resolve, so callers transparently get the decorator and delivery behaviour is unchanged.
var inner = services.LastOrDefault(d => d.ServiceType == typeof(ISmsSender))
?? throw new InvalidOperationException(
"AddDevelopmentOtpCapture must run after AddCrossCuttingSeams — no ISmsSender is registered.");
services.Remove(inner);
services.AddSingleton<ISmsSender>(sp => new DevCapturingSmsSender(
ResolveSender(sp, inner),
sp.GetRequiredService<DevOtpStore>()));
return services;
}
private static ISmsSender ResolveSender(IServiceProvider sp, ServiceDescriptor descriptor) => descriptor switch
{
{ ImplementationInstance: ISmsSender instance } => instance,
{ ImplementationFactory: { } factory } => (ISmsSender)factory(sp),
{ ImplementationType: { } type } => (ISmsSender)ActivatorUtilities.CreateInstance(sp, type),
_ => throw new InvalidOperationException("The registered ISmsSender cannot be constructed for decoration."),
};
/// <summary>
/// Development/Testing-only re-registration of the real <see cref="MockPaymentCaptureSimulator"/> over the
/// production <see cref="DisabledPaymentCaptureSimulator"/> (refinement-phase-8, 6.4). The <c>bookings/convert</c>
/// simulator path is a dev/test affordance — production converts via the real b10 webhook confirm, not this
/// command. Last registration wins, so callers transparently get the succeeding mock in Dev/Testing.
/// </summary>
public static IServiceCollection AddDevelopmentPaymentCapture(this IServiceCollection services)
{
services.AddSingleton<IPaymentCaptureSimulator, MockPaymentCaptureSimulator>();
return services;
}
}