mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-11-15 08:46:26 +01:00
Rename paywall to payment
This commit is contained in:
26
VoidCat/Services/Payment/CachePaymentOrderStore.cs
Normal file
26
VoidCat/Services/Payment/CachePaymentOrderStore.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using VoidCat.Model.Payments;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Payment;
|
||||
|
||||
/// <inheritdoc cref="IPaymentOrderStore"/>
|
||||
public class CachePaymentOrderStore : BasicCacheStore<PaymentOrder>, IPaymentOrderStore
|
||||
{
|
||||
public CachePaymentOrderStore(ICache cache) : base(cache)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask UpdateStatus(Guid order, PaymentOrderStatus status)
|
||||
{
|
||||
var old = await Get(order);
|
||||
if (old == default) return;
|
||||
|
||||
old.Status = status;
|
||||
|
||||
await Add(order, old);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string MapKey(Guid id) => $"payment:order:{id}";
|
||||
}
|
||||
28
VoidCat/Services/Payment/CachePaymentStore.cs
Normal file
28
VoidCat/Services/Payment/CachePaymentStore.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using VoidCat.Model.Payments;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Payment;
|
||||
|
||||
/// <inheritdoc cref="IPaymentStore"/>
|
||||
public class CachePaymentStore : BasicCacheStore<PaymentConfig>, IPaymentStore
|
||||
{
|
||||
public CachePaymentStore(ICache database)
|
||||
: base(database)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<PaymentConfig?> Get(Guid id)
|
||||
{
|
||||
var cfg = await Cache.Get<NoPaymentConfig>(MapKey(id));
|
||||
return cfg?.Service switch
|
||||
{
|
||||
PaymentServices.None => cfg,
|
||||
PaymentServices.Strike => await Cache.Get<StrikePaymentConfig>(MapKey(id)),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string MapKey(Guid id) => $"payment:config:{id}";
|
||||
}
|
||||
23
VoidCat/Services/Payment/PaymentFactory.cs
Normal file
23
VoidCat/Services/Payment/PaymentFactory.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using VoidCat.Model.Payments;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Payment;
|
||||
|
||||
public class PaymentFactory : IPaymentFactory
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public PaymentFactory(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public ValueTask<IPaymentProvider> CreateProvider(PaymentServices svc)
|
||||
{
|
||||
return ValueTask.FromResult<IPaymentProvider>(svc switch
|
||||
{
|
||||
PaymentServices.Strike => _services.GetRequiredService<StrikePaymentProvider>(),
|
||||
_ => throw new ArgumentException("Must have a payment config", nameof(svc))
|
||||
});
|
||||
}
|
||||
}
|
||||
32
VoidCat/Services/Payment/PaymentStartup.cs
Normal file
32
VoidCat/Services/Payment/PaymentStartup.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.Strike;
|
||||
|
||||
namespace VoidCat.Services.Payment;
|
||||
|
||||
public static class PaymentStartup
|
||||
{
|
||||
/// <summary>
|
||||
/// Add services required to use payment functions
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="settings"></param>
|
||||
public static void AddPaymentServices(this IServiceCollection services, VoidSettings settings)
|
||||
{
|
||||
services.AddTransient<IPaymentFactory, PaymentFactory>();
|
||||
if (settings.HasPostgres())
|
||||
{
|
||||
services.AddTransient<IPaymentStore, PostgresPaymentStore>();
|
||||
services.AddTransient<IPaymentOrderStore, PostgresPaymentOrderStore>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddTransient<IPaymentStore, CachePaymentStore>();
|
||||
services.AddTransient<IPaymentOrderStore, CachePaymentOrderStore>();
|
||||
}
|
||||
|
||||
// strike
|
||||
services.AddTransient<StrikeApi>();
|
||||
services.AddTransient<StrikePaymentProvider>();
|
||||
}
|
||||
}
|
||||
104
VoidCat/Services/Payment/PostgresPaymentOrderStore.cs
Normal file
104
VoidCat/Services/Payment/PostgresPaymentOrderStore.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using Dapper;
|
||||
using VoidCat.Model.Payments;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Payment;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class PostgresPaymentOrderStore : IPaymentOrderStore
|
||||
{
|
||||
private readonly PostgresConnectionFactory _connection;
|
||||
|
||||
public PostgresPaymentOrderStore(PostgresConnectionFactory connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PaymentOrder?> Get(Guid id)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
var order = await conn.QuerySingleOrDefaultAsync<DtoPaymentOrder>(
|
||||
@"select * from ""PaymentOrder"" where ""Id"" = :id", new {id});
|
||||
if (order.Service is PaymentServices.Strike)
|
||||
{
|
||||
var lnDetails = await conn.QuerySingleAsync<LightningPaymentOrder>(
|
||||
@"select * from ""PaymentOrderLightning"" where ""Order"" = :id", new
|
||||
{
|
||||
id = order.Id
|
||||
});
|
||||
return new LightningPaymentOrder
|
||||
{
|
||||
Id = order.Id,
|
||||
File = order.File,
|
||||
Price = new(order.Amount, order.Currency),
|
||||
Service = order.Service,
|
||||
Status = order.Status,
|
||||
Invoice = lnDetails.Invoice,
|
||||
Expire = lnDetails.Expire
|
||||
};
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IReadOnlyList<PaymentOrder>> Get(Guid[] ids)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Add(Guid id, PaymentOrder obj)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
await using var txn = await conn.BeginTransactionAsync();
|
||||
await conn.ExecuteAsync(
|
||||
@"insert into ""PaymentOrder""(""Id"", ""File"", ""Service"", ""Currency"", ""Amount"", ""Status"")
|
||||
values(:id, :file, :service, :currency, :amt, :status)",
|
||||
new
|
||||
{
|
||||
id,
|
||||
file = obj.File,
|
||||
service = (int) obj.Service,
|
||||
currency = (int) obj.Price.Currency,
|
||||
amt = obj.Price.Amount, // :amount wasn't working?
|
||||
status = (int) obj.Status
|
||||
});
|
||||
|
||||
if (obj is LightningPaymentOrder ln)
|
||||
{
|
||||
await conn.ExecuteAsync(
|
||||
@"insert into ""PaymentOrderLightning""(""Order"", ""Invoice"", ""Expire"") values(:order, :invoice, :expire)",
|
||||
new
|
||||
{
|
||||
order = id,
|
||||
invoice = ln.Invoice,
|
||||
expire = ln.Expire.ToUniversalTime()
|
||||
});
|
||||
}
|
||||
|
||||
await txn.CommitAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Delete(Guid id)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(@"delete from ""PaymentOrder"" where ""Id"" = :id", new {id});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask UpdateStatus(Guid order, PaymentOrderStatus status)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(@"update ""PaymentOrder"" set ""Status"" = :status where ""Id"" = :id",
|
||||
new {id = order, status = (int) status});
|
||||
}
|
||||
|
||||
private sealed class DtoPaymentOrder : PaymentOrder
|
||||
{
|
||||
public PaymentCurrencies Currency { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
}
|
||||
}
|
||||
93
VoidCat/Services/Payment/PostgresPaymentStore.cs
Normal file
93
VoidCat/Services/Payment/PostgresPaymentStore.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Dapper;
|
||||
using VoidCat.Model.Payments;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Payment;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class PostgresPaymentStore : IPaymentStore
|
||||
{
|
||||
private readonly PostgresConnectionFactory _connection;
|
||||
|
||||
public PostgresPaymentStore(PostgresConnectionFactory connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PaymentConfig?> Get(Guid id)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
var svc = await conn.QuerySingleOrDefaultAsync<DtoPaymentConfig>(
|
||||
@"select * from ""Payment"" where ""File"" = :file", new {file = id});
|
||||
if (svc != default)
|
||||
{
|
||||
switch (svc.Service)
|
||||
{
|
||||
case PaymentServices.Strike:
|
||||
{
|
||||
var handle =
|
||||
await conn.ExecuteScalarAsync<string>(
|
||||
@"select ""Handle"" from ""PaymentStrike"" where ""File"" = :file", new {file = id});
|
||||
return new StrikePaymentConfig
|
||||
{
|
||||
Cost = new(svc.Amount, svc.Currency),
|
||||
File = svc.File,
|
||||
Handle = handle,
|
||||
Service = PaymentServices.Strike
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IReadOnlyList<PaymentConfig>> Get(Guid[] ids)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Add(Guid id, PaymentConfig obj)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
await using var txn = await conn.BeginTransactionAsync();
|
||||
await conn.ExecuteAsync(
|
||||
@"insert into ""Payment""(""File"", ""Service"", ""Amount"", ""Currency"") values(:file, :service, :amount, :currency)
|
||||
on conflict(""File"") do update set ""Service"" = :service, ""Amount"" = :amount, ""Currency"" = :currency",
|
||||
new
|
||||
{
|
||||
file = id,
|
||||
service = (int)obj.Service,
|
||||
amount = obj.Cost.Amount,
|
||||
currency = obj.Cost.Currency
|
||||
});
|
||||
|
||||
if (obj is StrikePaymentConfig sc)
|
||||
{
|
||||
await conn.ExecuteAsync(@"insert into ""PaymentStrike""(""File"", ""Handle"") values(:file, :handle)
|
||||
on conflict(""File"") do update set ""Handle"" = :handle", new
|
||||
{
|
||||
file = id,
|
||||
handle = sc.Handle
|
||||
});
|
||||
}
|
||||
|
||||
await txn.CommitAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask Delete(Guid id)
|
||||
{
|
||||
await using var conn = await _connection.Get();
|
||||
await conn.ExecuteAsync(@"delete from ""Payment"" where ""File"" = :file", new {file = id});
|
||||
}
|
||||
|
||||
private sealed class DtoPaymentConfig : PaymentConfig
|
||||
{
|
||||
public PaymentCurrencies Currency { get; init; }
|
||||
public decimal Amount { get; init; }
|
||||
}
|
||||
}
|
||||
147
VoidCat/Services/Payment/StrikePaymentProvider.cs
Normal file
147
VoidCat/Services/Payment/StrikePaymentProvider.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System.Globalization;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Payments;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.Strike;
|
||||
|
||||
namespace VoidCat.Services.Payment;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class StrikePaymentProvider : IPaymentProvider
|
||||
{
|
||||
private readonly ILogger<StrikePaymentProvider> _logger;
|
||||
private readonly StrikeApi _strike;
|
||||
private readonly IPaymentOrderStore _orderStore;
|
||||
|
||||
public StrikePaymentProvider(ILogger<StrikePaymentProvider> logger, StrikeApi strike, IPaymentOrderStore orderStore)
|
||||
{
|
||||
_logger = logger;
|
||||
_strike = strike;
|
||||
_orderStore = orderStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PaymentOrder?> CreateOrder(PaymentConfig config)
|
||||
{
|
||||
IsStrikePayment(config, out var strikeConfig);
|
||||
_logger.LogInformation("Generating invoice for {Currency} {Amount}", config.Cost.Currency, config.Cost.Amount);
|
||||
|
||||
var currency = MapCurrency(strikeConfig.Cost.Currency);
|
||||
if (currency == Currencies.USD)
|
||||
{
|
||||
// map USD to USDT if USD is not available and USDT is
|
||||
var profile = await _strike.GetProfile(strikeConfig.Handle);
|
||||
if (profile != default)
|
||||
{
|
||||
var usd = profile.Currencies.FirstOrDefault(a => a.Currency == Currencies.USD);
|
||||
var usdt = profile.Currencies.FirstOrDefault(a => a.Currency == Currencies.USDT);
|
||||
if (!(usd?.IsAvailable ?? false) && (usdt?.IsAvailable ?? false))
|
||||
{
|
||||
currency = Currencies.USDT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invoice = await _strike.GenerateInvoice(new()
|
||||
{
|
||||
Handle = strikeConfig.Handle,
|
||||
Amount = new()
|
||||
{
|
||||
Amount = strikeConfig.Cost.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
Currency = currency
|
||||
},
|
||||
Description = config.File.ToBase58()
|
||||
});
|
||||
if (invoice != default)
|
||||
{
|
||||
var quote = await _strike.GetInvoiceQuote(invoice.InvoiceId);
|
||||
if (quote != default)
|
||||
{
|
||||
var order = new LightningPaymentOrder
|
||||
{
|
||||
Id = invoice.InvoiceId,
|
||||
File = config.File,
|
||||
Service = PaymentServices.Strike,
|
||||
Price = config.Cost,
|
||||
Status = PaymentOrderStatus.Unpaid,
|
||||
Invoice = quote.LnInvoice!,
|
||||
Expire = DateTime.SpecifyKind(quote.Expiration.DateTime, DateTimeKind.Utc)
|
||||
};
|
||||
await _orderStore.Add(order.Id, order);
|
||||
return order;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Failed to get quote for invoice: {Id}", invoice.InvoiceId);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Failed to get invoice for config: File={File}, Service={Service}", config.File,
|
||||
config.Service.ToString());
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<PaymentOrder?> GetOrderStatus(Guid id)
|
||||
{
|
||||
var order = await _orderStore.Get(id);
|
||||
if (order is {Status: PaymentOrderStatus.Paid or PaymentOrderStatus.Expired}) return order;
|
||||
|
||||
var providerOrder = await _strike.GetInvoice(id);
|
||||
if (providerOrder != default)
|
||||
{
|
||||
var status = MapStatus(providerOrder.State);
|
||||
await _orderStore.UpdateStatus(id, status);
|
||||
|
||||
return new()
|
||||
{
|
||||
Id = id,
|
||||
Price = new(decimal.Parse(providerOrder!.Amount!.Amount!),
|
||||
MapCurrency(providerOrder.Amount!.Currency!.Value)),
|
||||
Service = PaymentServices.Strike,
|
||||
Status = status
|
||||
};
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private PaymentOrderStatus MapStatus(InvoiceState providerOrderState)
|
||||
=> providerOrderState switch
|
||||
{
|
||||
InvoiceState.UNPAID => PaymentOrderStatus.Unpaid,
|
||||
InvoiceState.PENDING => PaymentOrderStatus.Unpaid,
|
||||
InvoiceState.PAID => PaymentOrderStatus.Paid,
|
||||
InvoiceState.CANCELLED => PaymentOrderStatus.Expired,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(providerOrderState), providerOrderState, null)
|
||||
};
|
||||
|
||||
private static Currencies MapCurrency(PaymentCurrencies c)
|
||||
=> c switch
|
||||
{
|
||||
PaymentCurrencies.BTC => Currencies.BTC,
|
||||
PaymentCurrencies.USD => Currencies.USD,
|
||||
PaymentCurrencies.EUR => Currencies.EUR,
|
||||
PaymentCurrencies.GBP => Currencies.GBP,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||
};
|
||||
|
||||
private static PaymentCurrencies MapCurrency(Currencies c)
|
||||
=> c switch
|
||||
{
|
||||
Currencies.BTC => PaymentCurrencies.BTC,
|
||||
Currencies.USD => PaymentCurrencies.USD,
|
||||
Currencies.USDT => PaymentCurrencies.USD,
|
||||
Currencies.EUR => PaymentCurrencies.EUR,
|
||||
Currencies.GBP => PaymentCurrencies.GBP,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||
};
|
||||
|
||||
private static void IsStrikePayment(PaymentConfig? cfg, out StrikePaymentConfig strikeConfig)
|
||||
{
|
||||
if (cfg?.Service != PaymentServices.Strike)
|
||||
{
|
||||
throw new ArgumentException("Must be strike Payment");
|
||||
}
|
||||
|
||||
strikeConfig = (cfg as StrikePaymentConfig)!;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user