feat(kromer): implement subscriptions - #9
Conversation
Laincy
left a comment
There was a problem hiding this comment.
I don't know enough about C# to comment on the actual quality so I'm sticking to general architectural stuff. Overall looks pretty good, Could perhaps use some more enforcing of invariants in the DB schema but I also don't know enough about Entity Framework to speak on how viable that actually is.
There was a problem hiding this comment.
Looks like this only processes the first 100 subscriptions that have lapsed, and only does so every minute. This probably won't cause many issues at runtime, but may cause processing to lag behind what's expected in cases that more than 100 have lapsed since the last sync.
Because this service also handles processing any subscriptions that would have lapsed during downtime, one could imagine this causing multiple minutes worth of backup depending on the amount of time that's passed and how many subscriptions there are.
Should be fixable by looping either here or in BillDueSubscriptionsAsync until the number of processed transactions is less than 100.
There was a problem hiding this comment.
You could consider parallelising or multithreading.
|
|
||
| try | ||
| { | ||
| await ChargeSubscriptionAsync(contract, subscription, subscriber, owner, now); |
There was a problem hiding this comment.
Having the new subscription start whenever its processed may cause issues with what service providers expect, especially for shorter subscriptions. This is similar to what I mentioned on the billing service and won't apply in most cases but it may also cause issues when re-syncing after downtime. It might be better to start them from whenever the previous period should have ended.
This also causes issues when re-syncing, because it doesn't account for any periods that should have occurred during downtime. It was discussed ages ago so you wouldn't know but we were going to treat these periods as if they had happened and billed subscribers for it. I don't see any issues with changing that to something else if you think it makes more sense but it's worth a discussion.
| .Select(q => q.Name); | ||
|
|
||
| var subscribedContracts = context.WalletSubscriptions | ||
| .Where(q => q.WalletAddress == address && q.Status == SubscriptionStatus.Active); |
There was a problem hiding this comment.
Subscribers that cancel their subscription while there is still time remaining should still be counted as subscribers and receive benefits for the remaining time on their subscription. Whenever we fetch active subscribers it should be done in a way that preserves this (e.g. subscriptions who's next_payment is before now)
Commenting this here but this applies across the entire PR
|
Not a critique of the implementation itself, but it would be nice to have more options for customizing who can subscribe to a contract:
|
|
@Laincy I think I've fixed most of the problems you mentioned! Care to take a peek again? |
|
As for what you've mentioned here above:
As for the other points, I'll implement them. |
use-case for restricting subscribers is for limited-access stuff, you wouldn't want people subscribing to stuff they aren't meant to, some programs might not deal well with that |
|
Right. I'll implement that too aswell then |
… migration not being visible
|
Implemented the subscription access control stuff: max subscriber caps, wallet allowlists, and closing contracts to new subscribers |
|
Here are a few issues I've noticed while trying to integrate subscriptions into Krawlet:
|
bananasov
left a comment
There was a problem hiding this comment.
Code looks good to me, missing a few doc comments on the new routes and some general comments on the code.
|
meow I think I fixed everything |
Ale32bit
left a comment
There was a problem hiding this comment.
Not too sure about the required keyword changes. Test it of course.
|
|
||
| public class WalletSubscriptionDto | ||
| { | ||
| public string Address { get; set; } = null!; |
There was a problem hiding this comment.
| public string Address { get; set; } = null!; | |
| public required string Address { get; set; } |
| { | ||
| public int Id { get; set; } | ||
|
|
||
| public string Receiver { get; set; } = null!; |
There was a problem hiding this comment.
| public string Receiver { get; set; } = null!; | |
| public required string Receiver { get; set; } |
|
|
||
| public string Receiver { get; set; } = null!; | ||
|
|
||
| public string BaseName { get; set; } = null!; |
There was a problem hiding this comment.
| public string BaseName { get; set; } = null!; | |
| public required string BaseName { get; set; } |
|
|
||
| public int PeriodMinutes { get; set; } | ||
|
|
||
| public string Description { get; set; } = null!; |
There was a problem hiding this comment.
| public string Description { get; set; } = null!; | |
| public required string Description { get; set; } |
| { | ||
| public int Id { get; set; } | ||
|
|
||
| public string Description { get; set; } = null!; |
There was a problem hiding this comment.
| public string Description { get; set; } = null!; | |
| public required string Description { get; set; } |
| } | ||
| } | ||
|
|
||
| private static string[]? NormalizeAllowedSubscribers(IEnumerable<string>? addresses) |
There was a problem hiding this comment.
Make it return List<string> or IEnumerable<string>.
| return normalized | ||
| .Distinct(StringComparer.Ordinal) | ||
| .OrderBy(q => q, StringComparer.Ordinal) | ||
| .ToArray(); |
There was a problem hiding this comment.
| .ToArray(); | |
| .ToList(); |
| NextPayment = subscription is not null && | ||
| (subscription.Status == SubscriptionStatus.Active || | ||
| (subscription.CancellationReason == ReasonUnsubscribed && | ||
| subscription.NextPayment > DateTime.UtcNow)) | ||
| ? subscription.NextPayment | ||
| : null, |
There was a problem hiding this comment.
Extract this condition before the call for clarity.
There was a problem hiding this comment.
Revert changes, use the user secrets.
There was a problem hiding this comment.
You could consider parallelising or multithreading.
Adds kromer V1 subscription contracts.
Contracts are owned by names/metanames. Creating/deleting a contract requires the name owner's
privatekey. When a name is transferred, contract ownership and future payments transfer with it.Subscriptions immediately charge then recur using a background billing worker.
New api endpoints are under
/api/v1/subscriptions:{ "privatekey", "name", "price", "period", "description" }foo,foo.kro,shop@foo.kro{ "privatekey": "..." }{ "privatekey": "..." }{ "privatekey": "..." }In the database, it adds
subscription_statusenum,subscription_contracts, andwallet_subscriptionsAdded a hand-built migration using entity framework core (pls test it rigorously, it seemed to work but can never be sure enough)
The API is based mostly on this: https://rentry.co/px7k93mf